/**
 * Copyright (c) 2006-2017, JGraph Holdings Ltd
 * Copyright (c) 2006-2017, draw.io AG
 */
(function()
{
	if (typeof html4 !== 'undefined')
	{
		/**
		 * Enables paste from Lucidchart
		 */
		html4.ATTRIBS['span::data-lucid-content'] = 0;
		html4.ATTRIBS['span::data-lucid-type'] = 0;
		
		/**
		 * Enables custom fonts in labels.
		 */
		html4.ATTRIBS['font::data-font-src'] = 0;
	}
	
	/**
	 * Definitions for sketch font styles.
	 */
	Editor.sketchFontFamily = 'Architects Daughter';
	Editor.sketchFontSource = 'https%3A%2F%2Ffonts.googleapis.com%2Fcss%3Ffamily%3DArchitects%2BDaughter';
	Editor.sketchFonts = [{'fontFamily': Editor.sketchFontFamily, 'fontUrl': decodeURIComponent(Editor.sketchFontSource)}];
	Editor.sketchDefaultCurveFitting = '1';
	Editor.sketchDefaultJiggle = '2';
		
	/**
	 * Icons for new UI style exported from https://fonts.google.com/icons (FFill: 0 Weight: 300 Grade: 0 Optical size: 48).
	 */
	Editor.thinFormatImage = '';
	Editor.thinGestureImage = '';
	Editor.thinShapesImage = '';
	Editor.thinUndoImage = '';
	Editor.thinRedoImage = '';
	Editor.thinDoubleArrowRightImage = '';
	Editor.thinNoteImage = '';
	Editor.thinTableImage = '';
	Editor.thinAddCircleImage = '';
	Editor.thinArrowLeftImage = '';
	Editor.thinArrowRightImage = '';
	Editor.thinVerticalDotsImage = '';
	Editor.thinDeleteImage = '';
	Editor.thinLightImage = '';
	Editor.thinDarkImage = '';
	Editor.thinCommentImage = '';
	Editor.thinMenuImage = '';
	Editor.thinViewImage = '';
	Editor.thinUserAddImage = '';
	Editor.thinUserFlashImage = '';
	Editor.thinShareImage = '';
	Editor.thinTextImage = '';
	Editor.thinRectangleImage = '';
	Editor.thinDataImage = '';
	Editor.thinExpandImage = '';
	Editor.thinGridImage = '';
	Editor.thinOpenImage = '';
	Editor.selectImage = '';

	/**
	 * 
	 */
	Editor.styles = [{},
		{commonStyle: {fontColor: '#393C56', strokeColor: '#E07A5F', fillColor: '#F2CC8F'},	
			graph: {background: '#F4F1DE', gridColor: '#D4D0C0'}},
		{vertexStyle: {strokeColor: '#BAC8D3', fillColor: '#09555B', fontColor: '#EEEEEE'},
			edgeStyle: {strokeColor: '#0B4D6A'}},
		{vertexStyle: {strokeColor: '#FFFFFF', fillColor: '#182E3E', fontColor: '#FFFFFF'},
			edgeStyle: {strokeColor: '#23445D'},
			graph: {background: '#FCE7CD', gridColor: '#CFBDA8'}},
		{vertexStyle: {strokeColor: '#D0CEE2', fillColor: '#5D7F99'},
			edgeStyle: {strokeColor: '#736CA8'},
			commonStyle: {fontColor: '#1A1A1A'}},
		{commonStyle: {fontColor: '#46495D', strokeColor: '#788AA3', fillColor: '#B2C9AB'}},
		{commonStyle: {fontColor: '#5AA9E6', strokeColor: '#FF6392', fillColor: '#FFE45E'}},
		{commonStyle: {fontColor: '#E4FDE1', strokeColor: '#028090', fillColor: '#F45B69'},
			graph: {background: '#114B5F', gridColor: '#0B3240'}},
		{commonStyle: {fontColor: '#FEFAE0', strokeColor: '#DDA15E', fillColor: '#BC6C25'},
			graph: {background: '#283618', gridColor: '#48632C'}},
			{commonStyle: {fontColor: '#143642', strokeColor: '#0F8B8D', fillColor: '#FAE5C7'},
			edgeStyle: {strokeColor: '#A8201A'},
			graph: {background: '#DAD2D8', gridColor: '#ABA4A9'}},
		{},
		{vertexStyle: {strokeColor: '#D0CEE2', fillColor: '#FAD9D5'},
			edgeStyle: {strokeColor: '#09555B'},
			commonStyle: {fontColor: '#1A1A1A'}},
		{commonStyle: {fontColor: '#1D3557', strokeColor: '#457B9D', fillColor: '#A8DADC'},	
			graph: {background: '#F1FAEE'}},
		{commonStyle: {fontColor: '#095C86', strokeColor: '#AF45ED', fillColor: '#F694C1'},
			edgeStyle: {strokeColor: '#60E696'}},
		{commonStyle: {fontColor: '#5C5C5C', strokeColor: '#006658', fillColor: '#21C0A5'}},
		{vertexStyle: {strokeColor: '#FFFFFF', fillColor: '#F08E81'},
			edgeStyle: {strokeColor: '#182E3E'},
			commonStyle: {fontColor: '#1A1A1A'},
			graph: {background: '#B0E3E6', gridColor: '#87AEB0'}},
		{vertexStyle: {strokeColor: '#909090', fillColor: '#F5AB50'},
			edgeStyle: {strokeColor: '#182E3E'},
			commonStyle: {fontColor: '#1A1A1A'},
			graph: {background: '#EEEEEE'}},
		{vertexStyle: {strokeColor: '#BAC8D3', fillColor: '#B1DDF0', fontColor: '#182E3E'},
			edgeStyle: {strokeColor: '#EEEEEE', fontColor: '#FFFFFF'},
			graph: {background: '#09555B', gridColor: '#13B4C2'}},
		{vertexStyle: {strokeColor: '#EEEEEE', fillColor: '#56517E', fontColor: '#FFFFFF'},
			edgeStyle: {strokeColor: '#182E3E'},
			graph: {background: '#FAD9D5', gridColor: '#BFA6A3'}},
		{vertexStyle: {fillColor: '#EEEEEE', fontColor: '#1A1A1A'},
			edgeStyle: {fontColor: '#FFFFFF'},
			commonStyle: {strokeColor: '#FFFFFF'},
			graph: {background: '#182E3E', gridColor: '#4D94C7'}}];
	
	/**
	 * 
	 */
	Editor.logoImage = '';
	Editor.saveImage = '';
	Editor.globeImage = '';
	Editor.commentImage = '';
	Editor.userImage = '';
	Editor.groupImage = '';
	Editor.syncImage = '';
	Editor.cloudImage = '';
	Editor.cloudOffImage = '';
	Editor.calendarImage = '';
 	Editor.syncProblemImage = '';
	Editor.tailSpin = '';
	Editor.mailImage = '';
	Editor.cameraImage = '';
	Editor.tagsImage = '';
	Editor.contrastImage = '';
	Editor.chevronUpImage = '';
	Editor.chevronDownImage = '';
	Editor.chevronLeftImage = '';
	Editor.chevronRightImage = '';
	Editor.rightPanelOpenImage = '';
	Editor.rightPanelCloseImage = '';
	Editor.spinImage = '';
	Editor.errorImage = '';
	Editor.smallPlusImage = '';
	Editor.hiResImage = '';
	Editor.loResImage = '';
	Editor.blankImage = '';
	Editor.facebookImage = IMAGE_PATH + '/facebook.png';
	Editor.tweetImage = IMAGE_PATH + '/tweet.png';
	
	/**
	 * Broken image symbol for offline SVG.
	 */
	Editor.svgBrokenImage = Graph.createSvgImage(10, 10, '<rect x="0" y="0" width="10" height="10" stroke="#000" fill="transparent"/><path d="m 0 0 L 10 10 L 0 10 L 10 0" stroke="#000" fill="transparent"/>');

	/**
	 * Error image for not found images
	 */	
	Editor.configurationKey = '.configuration';
		
	/**
	 * Error image for not found images
	 */	
	Editor.settingsKey = '.drawio-config';
	
	/**
	 * Default value for custom libraries in mxSettings.
	 */
	Editor.defaultCustomLibraries = [];
	
	/**
	 * Default value for custom libraries in mxSettings.
	 */
	Editor.enableCustomLibraries = true;
	
	/**
	 * Not yet implemented. Reading uncompressed supported.
	 */
	Editor.enableUncompressedLibraries = false;
	
	/**
	 * Specifies if custom properties should be enabled.
	 */
	Editor.enableCustomProperties = true;
		
	/**
	 * Specifies if the simple theme should be enabled. This theme can be used
	 * at runtime in the kennedy theme.
	 */
	Editor.enableSimpleTheme = true;
			
	/**
	 * Specifies if the URL should be rewritten to contain the selected page.
	 * Default is true for online app without embed.
	 */
	Editor.enableHashObjects = !mxClient.IS_CHROMEAPP && !EditorUi.isElectronApp &&
		urlParams['embed'] != '1' && window.top == window.self;
	
	/**
	 * Sets the default value for including a copy of the diagram.
	 * Default is true.
	 */
	Editor.defaultIncludeDiagram = true;

	/**
	 * Specifies if custom properties should be enabled.
	 */
	Editor.enableServiceWorker = urlParams['pwa'] != '0' &&
		'serviceWorker' in navigator && (urlParams['offline'] == '1' ||
		urlParams['enableSW'] == '1' ||
		/.*\.diagrams\.net$/.test(window.location.hostname) ||
		/.*\.draw\.io$/.test(window.location.hostname));

	// Checks access to service worker in sandboxed context
	try
	{
		Editor.enableServiceWorker && navigator.serviceWorker;
	}
	catch (e)
	{
		Editor.enableServiceWorker = false;
	}

	/**
	 * Specifies if web fonts are enabled.
	 */
	Editor.enableWebFonts = urlParams['safe-style-src'] != '1' && !window.mxIsElectron;

	/**
	 * Disables the shadow option in the format panel.
	 */
	Editor.enableShadowOption = !mxClient.IS_SF;

	/**
	 * Disables the export URL function.
	 */
	Editor.enableExportUrl = true;

	/**
	 * Disables fast real time collaboration while keeping slower real time collaboration enabled.
	 */
	Editor.enableRealtime = true;

	/**
	 * Enables cache for patches and Pusher for messages. Default is true.
	 */
	Editor.enableRealtimeCache = true;
	
	/**
	 * Enables P2P instead of Pusher for messages. (Ignored if enableRealtimeCache is false.)
	 * Default is false.
	 */
	Editor.p2pSyncNotify = false;
	
	/**
	 * Specifies if XML files should be compressed. Default is true.
	 */
	Editor.compressXml = true;

	/**
	 * Specifies if XML files should be compressed by default. Default is false.
	 */
	Editor.defaultCompressed = false;
	
	/**
	 * Specifies if XML files should be compressed. Default is true.
	 */
	Editor.oneDriveInlinePicker = (window.urlParams != null && window.urlParams['inlinePicker'] == '0') ? false : true;

	/**
	 * Specifies global variables.
	 */
	Editor.globalVars = null;

	/**
	 * Default border for image export (to allow for sketch style).
	 */
	Editor.defaultBorder = 5;

	/**
	 * Specifies if cell metadata should be added to SVG output. Default is false.
	 */
	Editor.addSvgMetadata = false;

	/**
	 * Specifies animations should be enabled. Default is true.
	 */
	Editor.enableAnimations = true;

	/**
	 * Specifies paste should be at the mouse pointer location. Default is true.
	 */
	Editor.pasteAtMousePointer = true;

	/**
	 * Specifies the default text style.
	 */
	Editor.defaultTextStyle = 'text;html=1;whiteSpace=wrap;strokeColor=none;fillColor=none;' +
		'align=center;verticalAlign=middle;rounded=0;';
	
	/**
	 * Specifies if ChatGPT should be enabled. Default is true only
	 * on app.diagrams.net (including test and preprod).
	 */
	Editor.enableChatGpt = (/test\.draw\.io$/.test(window.location.hostname)) ||
		(/preprod\.diagrams\.net$/.test(window.location.hostname)) ||
		(/embed\.diagrams\.net$/.test(window.location.hostname)) ||
		(/app\.diagrams\.net$/.test(window.location.hostname));

	/**
	 * Specifies the ChatGPT API key. Default is null.
	 */
	Editor.gptApiKey = (urlParams['gpt-api-key'] != null) ?
		decodeURIComponent(urlParams['gpt-api-key']) : null;

	/**
	 * Specifies the ChatGPT model. Default is 'gpt-3.5-turbo'.
	 */
	Editor.gptModel = (urlParams['gpt-model'] != null) ?
		decodeURIComponent(urlParams['gpt-model']) : 'gpt-3.5-turbo';
	
	/**
	 * Specifies the ChatGPT endpoint URL. Default is
	 * 'https://api.openai.com/v1/chat/completions'.
	 */
	Editor.gptUrl = (urlParams['gpt-url'] != null) ?
		decodeURIComponent(urlParams['gpt-url']) :
		'https://api.openai.com/v1/chat/completions';
	
	/**
	 * Specifies if data URIs should be replaced with SVG sub-trees in SVG export.
	 * Default is true.
	 */
	Editor.replaceSvgDataUris = true;
	
	/**
	 * Specifies if foreignObject alternate content should be replaced with an image
	 * of the HTML text. Default is true.
	 */
	Editor.foreignObjectImages = true;
		
	/**
	 * Specifies the theme to use for SVG files. Possible values are 'light',
	 * 'dark' and 'auto'. Default is 'auto'.
	 */
	Editor.svgFileTheme = 'auto';
	
	/**
	 * Specifies the scale used to rasterize SVG images. Default is 4.
	 */
	Editor.svgRasterScale = 4;
			
	/**
	 * Specifies the scale used to rasterize HTML markup. Default is 4.
	 */
	Editor.htmlRasterScale = 4;
	
	/**
	 * Reference to the config object passed to <configure>.
	 */
	Editor.config = null;

	/**
	 * Reference to the version of the last config object in
	 * <configure>. If this is different to the last version in
	 * mxSettings.parse, then the settings are reset.
	 */
	Editor.configVersion = null;

	/**
	 * Reference to the version of the last config object in
	 * <configure>. If this is different to the last version in
	 * mxSettings.parse, then the settings are reset.
	 */
	Editor.createInheritProperty = function(title, key)
	{
		return {
			name: key, dispName: title, type: 'bool', defVal: false, isVisible: function(state, format)
			{
				if (state.vertices.length == 1)
				{
					var model = format.editorUi.editor.graph.model;
					var parent = model.getParent(state.vertices[0]);

					return model.isEdge(parent) || model.isVertex(parent);
				}
				else
				{
					return false;
				}
			}, getValue: function(state, format)
			{
				return mxUtils.getValue(state.style, key) == 'inherit';
			}, valueChanged: function(newVal, input, state, format)
			{
				var graph = format.editorUi.editor.graph;

				if (newVal == '1')
				{
					graph.setCellStyles(key, 'inherit', state.vertices);
				}
				else
				{
					graph.setCellStyles(key, null, state.vertices);
				}
			}
		};
	};

	/**
	 * Common properties for all edges.
	 */
	Editor.commonProperties = [
		{name: 'enumerate', dispName: 'Enumerate', type: 'bool', defVal: false, onChange: function(graph)
		{
			graph.refresh();
		}},
		{name: 'enumerateValue', dispName: 'Enumerate Value', type: 'string', defVal: '', isVisible: function(state, format)
		{
			return mxUtils.getValue(state.style, 'enumerate', '0') == '1';
		}},
        {name: 'comic', dispName: 'Comic', type: 'bool', defVal: false, isVisible: function(state, format)
        {
        	return mxUtils.getValue(state.style, 'sketch', '0') != '1';
        }},
        {name: 'jiggle', dispName: 'Jiggle', type: 'float', min: 0, defVal: 1, isVisible: function(state, format)
        {
        	return mxUtils.getValue(state.style, 'comic', '0') == '1' ||
        		mxUtils.getValue(state.style, 'sketch', (urlParams['rough'] == '1') ? '1' : '0') == '1';
        }},
        {name: 'fillWeight', dispName: 'Fill Weight', type: 'int', defVal: -1, isVisible: function(state, format)
        {
        	return mxUtils.getValue(state.style, 'sketch', (urlParams['rough'] == '1') ? '1' : '0') == '1' &&
				state.vertices.length > 0;
        }},
        {name: 'hachureGap', dispName: 'Hachure Gap', type: 'int', defVal: -1, isVisible: function(state, format)
        {
        	return mxUtils.getValue(state.style, 'sketch', (urlParams['rough'] == '1') ? '1' : '0') == '1' &&
				state.vertices.length > 0;
        }},
        {name: 'hachureAngle', dispName: 'Hachure Angle', type: 'int', defVal: -41, isVisible: function(state, format)
        {
        	return mxUtils.getValue(state.style, 'sketch', (urlParams['rough'] == '1') ? '1' : '0') == '1' &&
				state.vertices.length > 0;
        }},
        {name: 'curveFitting', dispName: 'Curve Fitting', type: 'float', defVal: 0.95, isVisible: function(state, format)
        {
        	return mxUtils.getValue(state.style, 'sketch', (urlParams['rough'] == '1') ? '1' : '0') == '1';
        }},
        {name: 'simplification', dispName: 'Simplification', type: 'float', defVal: 0, min: 0, max: 1, isVisible: function(state, format)
        {
        	return mxUtils.getValue(state.style, 'sketch', (urlParams['rough'] == '1') ? '1' : '0') == '1';
        }},
        {name: 'disableMultiStroke', dispName: 'Disable Multi Stroke', type: 'bool', defVal: false, isVisible: function(state, format)
        {
        	return mxUtils.getValue(state.style, 'sketch', (urlParams['rough'] == '1') ? '1' : '0') == '1';
        }},
        {name: 'disableMultiStrokeFill', dispName: 'Disable Multi Stroke Fill', type: 'bool', defVal: false, isVisible: function(state, format)
        {
        	return mxUtils.getValue(state.style, 'sketch', (urlParams['rough'] == '1') ? '1' : '0') == '1' &&
				state.vertices.length > 0;
        }},
        {name: 'dashOffset', dispName: 'Dash Offset', type: 'int', defVal: -1, isVisible: function(state, format)
        {
        	return mxUtils.getValue(state.style, 'sketch', (urlParams['rough'] == '1') ? '1' : '0') == '1' &&
				state.vertices.length > 0;
        }},
        {name: 'dashGap', dispName: 'Dash Gap', type: 'int', defVal: -1, isVisible: function(state, format)
        {
        	return mxUtils.getValue(state.style, 'sketch', (urlParams['rough'] == '1') ? '1' : '0') == '1' &&
				state.vertices.length > 0;
        }},
        {name: 'zigzagOffset', dispName: 'ZigZag Offset', type: 'int', defVal: -1, isVisible: function(state, format)
        {
        	return mxUtils.getValue(state.style, 'sketch', (urlParams['rough'] == '1') ? '1' : '0') == '1' &&
				state.vertices.length > 0;
        }},
        {name: 'sketchStyle', dispName: 'Sketch Style', type: 'enum', defVal: 'rough',
        	enumList: [{val: 'rough', dispName: 'Rough'}, {val: 'comic', dispName: 'Comic'}],
        	isVisible: function(state, format)
        {
        	return mxUtils.getValue(state.style, 'sketch', (urlParams['rough'] == '1') ? '1' : '0') == '1';
        }},
		{name: mxConstants.STYLE_SHADOWCOLOR, dispName: 'Shadow Color', type: 'color', getDefaultValue: function()
		{
			return mxConstants.SHADOWCOLOR;
		}, isVisible: function(state, format)
		{
			return mxUtils.getValue(state.style, mxConstants.STYLE_SHADOW, '0') == '1' ||
				mxUtils.getValue(state.style, mxConstants.STYLE_TEXT_SHADOW, '0') == '1';
		}},
		{name: mxConstants.STYLE_SHADOW_OPACITY, dispName: 'Shadow Opacity', type: 'int', min: 0, max: 100, getDefaultValue: function()
		{
			return mxConstants.SHADOW_OPACITY * 100;
		}, isVisible: function(state, format)
		{
			return mxUtils.getValue(state.style, mxConstants.STYLE_SHADOW, '0') == '1' ||
				mxUtils.getValue(state.style, mxConstants.STYLE_TEXT_SHADOW, '0') == '1';
		}},
		{name: mxConstants.STYLE_SHADOW_OFFSET_X, dispName: 'Shadow Offset X', type: 'int', getDefaultValue: function()
		{
			return mxConstants.SHADOW_OFFSET_X;
		}, isVisible: function(state, format)
		{
			return mxUtils.getValue(state.style, mxConstants.STYLE_SHADOW, '0') == '1' ||
				mxUtils.getValue(state.style, mxConstants.STYLE_TEXT_SHADOW, '0') == '1';
		}},
		{name: mxConstants.STYLE_SHADOW_OFFSET_Y, dispName: 'Shadow Offset Y', type: 'int', getDefaultValue: function()
		{
			return mxConstants.SHADOW_OFFSET_Y;
		}, isVisible: function(state, format)
		{
			return mxUtils.getValue(state.style, mxConstants.STYLE_SHADOW, '0') == '1' ||
				mxUtils.getValue(state.style, mxConstants.STYLE_TEXT_SHADOW, '0') == '1';
		}},
		{name: mxConstants.STYLE_SHADOW_BLUR, dispName: 'Shadow Blur', type: 'int', min: 0, getDefaultValue: function()
		{
			return mxConstants.SHADOW_BLUR;
		}, isVisible: function(state, format)
		{
			return mxUtils.getValue(state.style, mxConstants.STYLE_SHADOW, '0') == '1' ||
				mxUtils.getValue(state.style, mxConstants.STYLE_TEXT_SHADOW, '0') == '1';
		}},
        {name: 'linecap', dispName: 'Line Cap', type: 'enum', defVal: null,
        	enumList: [{val: null, dispName: 'Flat'}, {val: 'round', dispName: 'Round'}, {val: 'square', dispName: 'Square'}]
        },
		{name: 'linejoin', dispName: 'Line Join', type: 'enum', defVal: null,
        	enumList: [{val: null, dispName: 'Miter'}, {val: 'arcs', dispName: 'Arcs'}, {val: 'bevel', dispName: 'Bevel'},
			{val: 'miter-clip', dispName: 'Miter-Clip'}, {val: 'round', dispName: 'Round'}]
        },
		{name: 'miterlimit', dispName: 'Miter Limit', type: 'int', min: 1, defVal: 4, isVisible: function(state, format)
		{
			var linejoin = mxUtils.getValue(state.style, 'linejoin', 'miter');

			return linejoin == 'miter' || linejoin == 'miter-clip';
		}}
	];
	
	/**
	 * Common properties for all edges.
	 */
	Editor.commonEdgeProperties = [
        {type: 'separator'},
        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: mxConstants.LINE_ARCSIZE},
        {name: 'sourcePortConstraint', dispName: 'Source Constraint', type: 'enum', defVal: 'none',
        	enumList: [{val: 'none', dispName: 'None'}, {val: 'north', dispName: 'North'}, {val: 'east', dispName: 'East'}, {val: 'south', dispName: 'South'}, {val: 'west', dispName: 'West'}]
        },
        {name: 'targetPortConstraint', dispName: 'Target Constraint', type: 'enum', defVal: 'none',
        	enumList: [{val: 'none', dispName: 'None'}, {val: 'north', dispName: 'North'}, {val: 'east', dispName: 'East'}, {val: 'south', dispName: 'South'}, {val: 'west', dispName: 'West'}]
        },
        {name: 'jettySize', dispName: 'Jetty Size', type: 'int', min: 0, defVal: 'auto', allowAuto: true, isVisible: function(state)
        {
    		return mxUtils.getValue(state.style, mxConstants.STYLE_EDGE, null) == 'orthogonalEdgeStyle';
        }},
        {name: 'segment', dispName: 'Segment Size', type: 'int', min: 0, defVal: mxConstants.ENTITY_SEGMENT, isVisible: function(state)
        {
    		return mxUtils.getValue(state.style, mxConstants.STYLE_EDGE, null) == 'entityRelationEdgeStyle';
        }},
        {name: 'fillOpacity', dispName: 'Fill Opacity', type: 'int', min: 0, max: 100, defVal: 100},
        {name: 'strokeOpacity', dispName: 'Stroke Opacity', type: 'int', min: 0, max: 100, defVal: 100},
        {name: 'startFill', dispName: 'Start Fill', type: 'bool', defVal: true},
		{name: 'startFillColor', dispName: 'Start Fill Color', type: 'color', defVal: null},
        {name: 'endFill', dispName: 'End Fill', type: 'bool', defVal: true},
		{name: 'endFillColor', dispName: 'End Fill Color', type: 'color', defVal: null},
        {name: 'perimeterSpacing', dispName: 'Terminal Spacing', type: 'float', defVal: 0},
        {name: 'anchorPointDirection', dispName: 'Anchor Direction', type: 'bool', defVal: true},
        {name: 'snapToPoint', dispName: 'Snap to Point', type: 'bool', defVal: false},
        {name: 'dashPattern', dispName: 'Dash Pattern', type: 'numbers', defVal: ''},
        {name: 'fixDash', dispName: 'Fixed Dash', type: 'bool', defVal: false},
		{name: 'flowAnimationDuration', dispName: 'Flow Duration', type: 'int', defVal: 500, isVisible: function(state)
		{
			return mxUtils.getValue(state.style, 'flowAnimation', null) == '1';
		}},
		{name: 'flowAnimationTimingFunction', dispName: 'Flow Timing', type: 'enum', defVal: 'linear',
			enumList: [{val: 'linear', dispName: 'Linear'}, {val: 'ease', dispName: 'Ease'}, {val: 'ease-in', dispName: 'Ease-in'},
			{val: 'ease-out', dispName: 'Ease-out'}, {val: 'ease-in-out', dispName: 'Ease-in-out'}], isVisible: function(state)
			{
				return mxUtils.getValue(state.style, 'flowAnimation', null) == '1';
			}
		},
		{name: 'flowAnimationDirection', dispName: 'Flow Direction', type: 'enum', defVal: 'normal',
			enumList: [{val: 'normal', dispName: 'Normal'}, {val: 'reverse', dispName: 'Reverse'},
			{val: 'alternate', dispName: 'Alternate'}, {val: 'alternate-reverse', dispName: 'Alternate-Reverse'}],
			isVisible: function(state)
			{
				return mxUtils.getValue(state.style, 'flowAnimation', null) == '1';
			}
		},
		{name: 'editable', dispName: 'Editable', type: 'bool', defVal: true},
        {name: 'metaEdit', dispName: 'Edit Dialog', type: 'bool', defVal: false},
        {name: 'backgroundOutline', dispName: 'Background Outline', type: 'bool', defVal: false},
        {name: 'bendable', dispName: 'Bendable', type: 'bool', defVal: true},
        {name: 'movable', dispName: 'Movable', type: 'bool', defVal: true},
        {name: 'cloneable', dispName: 'Cloneable', type: 'bool', defVal: true},
        {name: 'deletable', dispName: 'Deletable', type: 'bool', defVal: true},
        {name: 'noJump', dispName: 'No Jumps', type: 'bool', defVal: false},
		{name: 'ignoreEdge', dispName: 'Ignore Edge', type: 'bool', defVal: false},
        {name: 'orthogonalLoop', dispName: 'Loop Routing', type: 'bool', defVal: false},
		{name: 'orthogonal', dispName: 'Orthogonal', type: 'bool', defVal: false}
	].concat(Editor.commonProperties);

	/**
	 * Common properties for all vertices.
	 */
	Editor.commonVertexProperties = [
        {name: 'colspan', dispName: 'Colspan', type: 'int', min: 1, defVal: 1, isVisible: function(state, format)
        {
        	var graph = format.editorUi.editor.graph;
        	
    		return state.vertices.length == 1 && state.edges.length == 0 && graph.isTableCell(state.vertices[0]);
        }},
        {name: 'rowspan', dispName: 'Rowspan', type: 'int', min: 1, defVal: 1, isVisible: function(state, format)
        {
        	var graph = format.editorUi.editor.graph;
        	
    		return state.vertices.length == 1 && state.edges.length == 0 && graph.isTableCell(state.vertices[0]);
        }},
        {type: 'separator'},
        {name: 'resizeLastRow', dispName: 'Resize Last Row', type: 'bool', getDefaultValue: function(state, format)
        {
        	var cell = (state.vertices.length == 1 && state.edges.length == 0) ? state.vertices[0] : null;
        	var graph = format.editorUi.editor.graph;
        	var style = graph.getCellStyle(cell);
        	
        	return mxUtils.getValue(style, 'resizeLastRow', '0') == '1';
        }, isVisible: function(state, format)
        {
        	var graph = format.editorUi.editor.graph;
        	
    		return state.vertices.length == 1 && state.edges.length == 0 &&
    			graph.isTable(state.vertices[0]);
        }},
        {name: 'resizeLast', dispName: 'Resize Last Column', type: 'bool', getDefaultValue: function(state, format)
        {
        	var cell = (state.vertices.length == 1 && state.edges.length == 0) ? state.vertices[0] : null;
        	var graph = format.editorUi.editor.graph;
        	var style = graph.getCellStyle(cell);
        	
        	return mxUtils.getValue(style, 'resizeLast', '0') == '1';
        }, isVisible: function(state, format)
        {
        	var graph = format.editorUi.editor.graph;
        	
    		return state.vertices.length == 1 && state.edges.length == 0 &&
    			graph.isTable(state.vertices[0]);
        }},
        {name: 'fillOpacity', dispName: 'Fill Opacity', type: 'int', min: 0, max: 100, defVal: 100},
        {name: 'strokeOpacity', dispName: 'Stroke Opacity', type: 'int', min: 0, max: 100, defVal: 100},
        {name: 'overflow', dispName: 'Text Overflow', defVal: 'visible', type: 'enum',
        	enumList: [{val: 'visible', dispName: 'Visible'}, {val: 'hidden', dispName: 'Hidden'}, {val: 'block', dispName: 'Block'},
        		{val: 'fill', dispName: 'Fill'}, {val: 'width', dispName: 'Width'}]
        },
        {name: 'noLabel', dispName: 'Hide Label', type: 'bool', defVal: false},
        {name: 'labelPadding', dispName: 'Label Padding', type: 'float', defVal: 0},
        {name: 'direction', dispName: 'Direction', type: 'enum', defVal: 'east',
        	enumList: [{val: 'north', dispName: 'North'}, {val: 'east', dispName: 'East'}, {val: 'south', dispName: 'South'}, {val: 'west', dispName: 'West'}]
        },
        {name: 'portConstraint', dispName: 'Constraint', type: 'enum', defVal: 'none',
        	enumList: [{val: 'none', dispName: 'None'}, {val: 'north', dispName: 'North'}, {val: 'east', dispName: 'East'}, {val: 'south', dispName: 'South'}, {val: 'west', dispName: 'West'}]
        },
        {name: 'portConstraintRotation', dispName: 'Rotate Constraint', type: 'bool', defVal: false},
        {name: 'connectable', dispName: 'Connectable', type: 'bool', getDefaultValue: function(state, format)
        {
        	var cell = (state.vertices.length > 0 && state.edges.length == 0) ? state.vertices[0] : null;
        	var graph = format.editorUi.editor.graph;
        	
        	return graph.isCellConnectable(cell);
        }, isVisible: function(state, format)
        {
    		return state.vertices.length > 0 && state.edges.length == 0;
        }},
        {name: 'allowArrows', dispName: 'Allow Arrows', type: 'bool', defVal: true},
        {name: 'snapToPoint', dispName: 'Snap to Point', type: 'bool', defVal: false},
        {name: 'perimeter', dispName: 'Perimeter', defVal: 'none', type: 'enum',
        	enumList: [{val: 'none', dispName: 'None'},
        			{val: 'rectanglePerimeter', dispName: 'Rectangle'}, {val: 'ellipsePerimeter', dispName: 'Ellipse'},
        			{val: 'rhombusPerimeter', dispName: 'Rhombus'}, {val: 'trianglePerimeter', dispName: 'Triangle'},
        			{val: 'hexagonPerimeter2', dispName: 'Hexagon'}, {val: 'lifelinePerimeter', dispName: 'Lifeline'},
        			{val: 'orthogonalPerimeter', dispName: 'Orthogonal'}, {val: 'backbonePerimeter', dispName: 'Backbone'},
        			{val: 'calloutPerimeter', dispName: 'Callout'}, {val: 'parallelogramPerimeter', dispName: 'Parallelogram'},
        			{val: 'trapezoidPerimeter', dispName: 'Trapezoid'}, {val: 'stepPerimeter', dispName: 'Step'},
        			{val: 'centerPerimeter', dispName: 'Center'}]
        },
        {name: 'fixDash', dispName: 'Fixed Dash', type: 'bool', defVal: false},
        {name: 'container', dispName: 'Container', type: 'bool', getDefaultValue: function(state, format)
        {
        	var cell = (state.vertices.length > 0 && state.edges.length == 0) ? state.vertices[0] : null;
        	var graph = format.editorUi.editor.graph;

			return cell != null && graph.isSwimlane(cell);
		}, isVisible: function(state, format)
        {
    		return state.vertices.length > 0 && state.edges.length == 0;
        }},
        {name: 'dropTarget', dispName: 'Drop Target', type: 'bool', getDefaultValue: function(state, format)
        {
        	var cell = (state.vertices.length == 1 && state.edges.length == 0) ? state.vertices[0] : null;
        	var graph = format.editorUi.editor.graph;
        	
        	return cell != null && (graph.isSwimlane(cell) || graph.model.getChildCount(cell) > 0);
        }, isVisible: function(state, format)
        {
    		return state.vertices.length == 1 && state.edges.length == 0;
        }},
        {name: 'collapsible', dispName: 'Collapsible', type: 'bool', getDefaultValue: function(state, format)
        {
        	var cell = (state.vertices.length == 1 && state.edges.length == 0) ? state.vertices[0] : null;
        	var graph = format.editorUi.editor.graph;
        	
        	return cell != null && ((graph.isContainer(cell) && state.style['collapsible'] != '0') ||
        		(!graph.isContainer(cell) && state.style['collapsible'] == '1'));
        }, isVisible: function(state, format)
        {
    		return state.vertices.length == 1 && state.edges.length == 0;
        }},
        {name: 'recursiveResize', dispName: 'Resize Children', type: 'bool', defVal: true, isVisible: function(state, format)
        {
    		return state.vertices.length == 1 && state.edges.length == 0 &&
    			!format.editorUi.editor.graph.isSwimlane(state.vertices[0]) &&
    			mxUtils.getValue(state.style, 'childLayout', null) == null;
        }},
        {name: 'expand', dispName: 'Expand', type: 'bool', defVal: true},
        {name: 'part', dispName: 'Part', type: 'bool', defVal: false, isVisible: function(state, format)
        {
        	var model = format.editorUi.editor.graph.model;
        	
        	return (state.vertices.length > 0) ? model.isVertex(model.getParent(state.vertices[0])) : false;
        }},
        {name: 'editable', dispName: 'Editable', type: 'bool', defVal: true},
        {name: 'metaEdit', dispName: 'Edit Dialog', type: 'bool', defVal: false},
        {name: 'backgroundOutline', dispName: 'Background Outline', type: 'bool', defVal: false},
        {name: 'movable', dispName: 'Movable', type: 'bool', defVal: true},
        {name: 'movableLabel', dispName: 'Movable Label', type: 'bool', defVal: false, isVisible: function(state, format)
        {
    		var geo = (state.vertices.length > 0) ? format.editorUi.editor.graph.getCellGeometry(state.vertices[0]) : null;
    		
    		return geo != null && !geo.relative;
        }},
        {name: 'autosize', dispName: 'Autosize', type: 'bool', defVal: false},
		{name: 'autosizeGrid', dispName: 'Autosize Grid', type: 'enum', defVal: null,
        	enumList: [{val: null, dispName: 'Default'}, {val: '1', dispName: 'Enabled'}, {val: '0', dispName: 'Disabled'}], isVisible: function(state, format)
			{
				return state.vertices.length > 0 && format.editorUi.editor.graph.isAutoSizeCell(state.vertices[0]);
			}
        },
        {name: 'fixedWidth', dispName: 'Fixed Width', type: 'bool', defVal: false},
        {name: 'resizable', dispName: 'Resizable', type: 'bool', defVal: true},
        {name: 'resizeWidth', dispName: 'Resize Width', type: 'bool', defVal: false},
        {name: 'resizeHeight', dispName: 'Resize Height', type: 'bool', defVal: false},
        {name: 'rotatable', dispName: 'Rotatable', type: 'bool', defVal: true},
        {name: 'cloneable', dispName: 'Cloneable', type: 'bool', defVal: true},
        {name: 'deletable', dispName: 'Deletable', type: 'bool', defVal: true},
        {name: 'treeFolding', dispName: 'Tree Folding', type: 'bool', defVal: false},
        {name: 'treeMoving', dispName: 'Tree Moving', type: 'bool', defVal: false},
        {name: 'pointerEvents', dispName: 'Pointer Events', type: 'bool', defVal: true, isVisible: function(state, format)
        {
        	var fillColor = mxUtils.getValue(state.style, mxConstants.STYLE_FILLCOLOR, null);
        	
        	return !mxShape.forceFilledPointerEvents ||
				(format.editorUi.editor.graph.isSwimlane(state.vertices[0]) ||
        		fillColor == null || fillColor == mxConstants.NONE ||
				mxUtils.getValue(state.style, mxConstants.STYLE_FILL_OPACITY, 100) == 0 ||
				mxUtils.getValue(state.style, mxConstants.STYLE_OPACITY, 100) == 0 ||
				state.style['pointerEvents'] != null);
        }},
        {name: 'moveCells', dispName: 'Move Cells on Fold', type: 'bool', defVal: false, isVisible: function(state, format)
        {
        	return state.vertices.length > 0 && format.editorUi.editor.graph.isContainer(state.vertices[0]);
        }},
		Editor.createInheritProperty('Inherit Font Family', mxConstants.STYLE_FONTFAMILY),
		Editor.createInheritProperty('Inherit Font Size', mxConstants.STYLE_FONTSIZE),
		Editor.createInheritProperty('Inherit Font Color', mxConstants.STYLE_FONTCOLOR),
		Editor.createInheritProperty('Inherit Fill Color', mxConstants.STYLE_FILLCOLOR),
		Editor.createInheritProperty('Inherit Stroke Color', mxConstants.STYLE_STROKECOLOR)
	].concat(Editor.commonProperties);

	/**
	 * Default value for the CSV import dialog.
	 */
	Editor.defaultCsvValue = '##\n' +
		'## Example CSV import. Use ## for comments and # for configuration. Paste CSV below.\n' +
		'## The following names are reserved and should not be used (or ignored):\n' +
		'## id, tooltip, placeholder(s), link and label (see below)\n' +
		'##\n' +
		'#\n' +
		'## Node label with placeholders and HTML.\n' +
		'## Default is \'%name_of_first_column%\'.\n' +
		'#\n' +
		'# label: %name%<br><i style="color:gray;">%position%</i><br><a href="mailto:%email%">Email</a>\n' +
		'#\n' +
		'## Node style (placeholders are replaced once).\n' +
		'## Default is the current style for nodes.\n' +
		'#\n' +
		'# style: label;image=%image%;whiteSpace=wrap;html=1;rounded=1;fillColor=%fill%;strokeColor=%stroke%;\n' +
		'#\n' +
		'## Parent style for nodes with child nodes (placeholders are replaced once).\n' +
		'#\n' +
		'# parentstyle: swimlane;whiteSpace=wrap;html=1;childLayout=stackLayout;horizontal=1;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;\n' +
		'#\n' +
		'## Style to be used for objects not in the CSV. If this is - then such objects are ignored,\n' +
		'## else they are created using this as their style, eg. whiteSpace=wrap;html=1;\n' +
		'#\n' +
		'# unknownStyle: -\n' +
		'#\n' +
		'## Optional column name that contains a reference to a named style in styles.\n' +
		'## Default is the current style for nodes.\n' +
		'#\n' +
		'# stylename: -\n' +
		'#\n' +
		'## JSON for named styles of the form {"name": "style", "name": "style"} where style is a cell style with\n' +
		'## placeholders that are replaced once.\n' +
		'#\n' +
		'# styles: -\n' +
		'#\n' +
		'## JSON for variables in styles of the form {"name": "value", "name": "value"} where name is a string\n' +
		'## that will replace a placeholder in a style.\n' +
		'#\n' +
		'# vars: -\n' +
		'#\n' +
		'## Optional column name that contains a reference to a named label in labels.\n' +
		'## Default is the current label.\n' +
		'#\n' +
		'# labelname: -\n' +
		'#\n' +
		'## JSON for named labels of the form {"name": "label", "name": "label"} where label is a cell label with\n' +
		'## placeholders.\n' +
		'#\n' +
		'# labels: -\n' +
		'#\n' +
		'## Uses the given column name as the identity for cells (updates existing cells).\n' +
		'## Default is no identity (empty value or -).\n' +
		'#\n' +
		'# identity: -\n' +
		'#\n' +
		'## Uses the given column name as the parent reference for cells. Default is no parent (empty or -).\n' +
		'## The identity above is used for resolving the reference so it must be specified.\n' +
		'#\n' +
		'# parent: -\n' +
		'#\n' +
		'## Adds a prefix to the identity of cells to make sure they do not collide with existing cells (whose\n' +
		'## IDs are numbers from 0..n, sometimes with a GUID prefix in the context of realtime collaboration).\n' +
		'## Default is csvimport-.\n' +
		'#\n' +
		'# namespace: csvimport-\n' +
		'#\n' +
		'## Connections between rows ("from": source colum, "to": target column).\n' +
		'## Label, style and invert are optional. Defaults are \'\', current style and false.\n' +
		'## If placeholders are used in the style, they are replaced with data from the source.\n' +
		'## An optional placeholders can be set to target to use data from the target instead.\n' +
		'## In addition to label, an optional fromlabel and tolabel can be used to name the column\n' +
		'## that contains the text for the label in the edges source or target (invert ignored).\n' +
		'## In addition to those, an optional source and targetlabel can be used to specify a label\n' +
		'## that contains placeholders referencing the respective columns in the source or target row.\n' +
		'## The label is created in the form fromlabel + sourcelabel + label + tolabel + targetlabel.\n' +
		'## Additional labels can be added by using an optional labels array with entries of the\n' +
		'## form {"label": string, "x": number, "y": number, "dx": number, "dy": number} where\n' +
		'## x is from -1 to 1 along the edge, y is orthogonal, and dx/dy are offsets in pixels.\n' +
		'## An optional placeholders with the string value "source" or "target" can be specified\n' +
		'## to replace placeholders in the additional label with data from the source or target.\n' +
		'## An optional data object can be specified to define the metadata for the connector.\n' +
		'## The target column may contain a comma-separated list of values.\n' +
		'## Multiple connect entries are allowed.\n' +
		'#\n' +
		'# connect: {"from": "manager", "to": "name", "invert": true, "label": "manages", \\\n' +
		'#          "style": "curved=1;endArrow=blockThin;endFill=1;fontSize=11;"}\n' +
		'# connect: {"from": "refs", "to": "id", "style": "curved=1;fontSize=11;"}\n' +
		'#\n' +
		'## Node x-coordinate. Possible value is a column name. Default is empty. Layouts will\n' +
		'## override this value.\n' +
		'#\n' +
		'# left: \n' +
		'#\n' +
		'## Node y-coordinate. Possible value is a column name. Default is empty. Layouts will\n' +
		'## override this value.\n' +
		'#\n' +
		'# top: \n' +
		'#\n' +
		'## Node width. Possible value is a number (in px), auto or an @ sign followed by a column\n' +
		'## name that contains the value for the width. Default is auto.\n' +
		'#\n' +
		'# width: auto\n' +
		'#\n' +
		'## Node height. Possible value is a number (in px), auto, width or an @ sign followed by a column\n' +
		'## name that contains the value for the height. Default is auto.\n' +
		'#\n' +
		'# height: auto\n' +
		'#\n' +
		'## Collapsed state for vertices. Possible values are true or false. Default is false.\n' +
		'#\n' +
		'# collapsed: false\n' +
		'#\n' +
		'## Padding for autosize. Default is 0.\n' +
		'#\n' +
		'# padding: -12\n' +
		'#\n' +
		'## Comma-separated list of ignored columns for metadata. (These can be\n' +
		'## used for connections and styles but will not be added as metadata.)\n' +
		'#\n' +
		'# ignore: id,image,fill,stroke,refs,manager\n' +
		'#\n' +
		'## Column to be renamed to link attribute (used as link).\n' +
		'#\n' +
		'# link: url\n' +
		'#\n' +
		'## Spacing between nodes. Default is 40.\n' +
		'#\n' +
		'# nodespacing: 40\n' +
		'#\n' +
		'## Spacing between levels of hierarchical layouts. Default is 100.\n' +
		'#\n' +
		'# levelspacing: 100\n' +
		'#\n' +
		'## Spacing between parallel edges. Default is 40. Use 0 to disable.\n' +
		'#\n' +
		'# edgespacing: 40\n' +
		'#\n' +
		'## Name or JSON of layout. Possible values are auto, none, verticaltree, horizontaltree,\n' +
		'## verticalflow, horizontalflow, organic, circle, orgchart or a JSON string as used in\n' +
		'## Layout, Apply. Default is auto.\n' +
		'#\n' +
		'# layout: auto\n' +
		'#\n' +
		'## ---- CSV below this line. First line are column names. ----\n' +
		'name,position,id,location,manager,email,fill,stroke,refs,url,image\n' +
		'Tessa Miller,CFO,emi,Office 1,,me@example.com,default,#6c8ebf,,https://app.diagrams.net,https://cdn3.iconfinder.com/data/icons/user-avatars-1/512/users-3-128.png\n' +
		'Edward Morrison,Brand Manager,emo,Office 2,Tessa Miller,me@example.com,default,#82b366,,https://app.diagrams.net,https://cdn3.iconfinder.com/data/icons/user-avatars-1/512/users-10-3-128.png\n' +
		'Alison Donovan,System Admin,rdo,Office 3,Tessa Miller,me@example.com,default,#82b366,"emo,tva",https://app.diagrams.net,https://cdn3.iconfinder.com/data/icons/user-avatars-1/512/users-2-128.png\n' +
		'Evan Valet,HR Director,tva,Office 4,Tessa Miller,me@example.com,default,#82b366,,https://app.diagrams.net,https://cdn3.iconfinder.com/data/icons/user-avatars-1/512/users-9-2-128.png\n';

	/**
	 * Capability check for canvas API
	 */
	Editor.canvasSupported = false;
	
	(function()
	{
		try
		{
			var cnv = document.createElement('canvas');
			Editor.canvasSupported = !!(cnv.getContext && cnv.getContext('2d'));
		}
		catch (e)
		{
			// ignore
		}
	})();
	
	/**
	 * Capability check for canvas export
	 */
	Editor.useCanvasForExport = false;

	(function()
	{
		try
		{
			var canvas = document.createElement('canvas');
			var img = new Image();
			
			// LATER: Capability check should not be async
			img.onload = function()
			{
				try
				{
					var ctx = canvas.getContext('2d');
					ctx.drawImage(img, 0, 0);

					// Works in Chrome, Firefox, Edge, Safari and Opera
					var result = canvas.toDataURL('image/png');
					Editor.useCanvasForExport = result != null && result.length > 6;
				}
				catch (e)
				{
					// ignore
				}
			};

			// Checks if SVG with foreignObject can be exported
			var svg = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1px" height="1px" version="1.1"><foreignObject pointer-events="all" width="1" height="1"><div xmlns="http://www.w3.org/1999/xhtml"></div></foreignObject></svg>';
			img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svg)));
		}
		catch (e)
		{
			// ignore
		}
	})();

	/**
	 * Capability check for canvas export formats
	 */
	Editor.jpgSupported = false;
	Editor.webpSupported = false;
	
	(function()
	{
		// Checks for client-side JPG and WebP support
		try
		{
		    var canvas = document.createElement('canvas');
		    canvas.width = canvas.height = 1;
			
		    var uri = canvas.toDataURL('image/jpeg');
		    Editor.jpgSupported = (uri.match('image/jpeg') !== null);

			uri = canvas.toDataURL('image/webp');
		    Editor.webpSupported = (uri.match('image/webp') !== null);
		}
		catch (e)
		{
			// ignore
		}
	})();

	/**
	 * Replaces light-dark color functions with light colors for older browsers.
	 * See https://caniuse.com/mdn-css_types_color_light-dark
	 * TODO: Remove this in January 2027.
	 */
	Editor.loadCompatibleCss = function(cssFile)
	{
		if (!mxUtils.lightDarkColorSupported)
		{
			cssFile = cssFile || 'styles/grapheditor.css';

			function replaceLightDarkFunctions(cssString)
			{
				var compatCss = cssString.replace(/light-dark\(\s*(var\([^\)]+\)|[^,]+)\s*,\s*(var\([^\)]+\)|[^)]+)\s*\)/g, '$1');

				// Replace :has with :not and adds additional work arounds
				// see https://caniuse.com/css-has
				try
				{
					if (!CSS.supports('selector(:has(*))'))
					{
						compatCss = compatCss.replace(/:has\(/g, ':not(') + '\n' +
							'.geFormatTitleContainer { overflow: hidden !important; }\n' +
							'.geRuler+.geRuler { margin-top: 14px !important; }\n' +
							'.geDiagramContainer { margin: 0 !important; }';
					}
				}
				catch (e)
				{
					// Ignore errors in unsupported browsers
				}

				return compatCss;
			};

			mxUtils.get(cssFile, mxUtils.bind(this, function(req)
			{
				if (req.getStatus() >= 200 && req.getStatus() <= 299)
				{
					var style = document.createElement('style');
					style.type = 'text/css';
					style.appendChild(document.createTextNode(
						replaceLightDarkFunctions(req.getText())));
					document.head.appendChild(style);
				}
			}));
		}
	};

	/**
	 * Compresses the given string.
	 */
	Editor.createRoughCanvas = function(c)
	{
		var rc = rough.canvas(
		{
			// Provides expected function but return value is not used
			getContext: function()
			{
				return c;
			}
		});
		
		rc.draw = function(drawable)
		{
			var sets = drawable.sets || [];
			var o = drawable.options || this.getDefaultOptions();

			for (var i = 0; i < sets.length; i++)
			{
				var drawing = sets[i];
				
				switch (drawing.type)
				{
					case 'path':
						if (o.stroke != null)
						{
							this._drawToContext(c, drawing, o);
						}
						break;
					case 'fillPath':
						this._drawToContext(c, drawing, o);
						break;
					case 'fillSketch':
						this.fillSketch(c, drawing, o);
						break;
				}
			}
		};
	
		rc.fillSketch = function(ctx, drawing, o)
		{
			var strokeColor = c.state.strokeColor;
			var strokeWidth = c.state.strokeWidth;
			var strokeAlpha = c.state.strokeAlpha;
			var dashed = c.state.dashed;
			
			var fweight = o.fillWeight;
			if (fweight < 0)
			{
				fweight = o.strokeWidth / 2;
			}

			c.setStrokeAlpha(c.state.fillAlpha);
			c.setStrokeColor(o.fill || '');
			c.setStrokeWidth(fweight);
			c.setDashed(false);
			
			this._drawToContext(ctx, drawing, o);
			
			c.setDashed(dashed);
			c.setStrokeWidth(strokeWidth);
			c.setStrokeColor(strokeColor);
			c.setStrokeAlpha(strokeAlpha);
		};
	
		rc._drawToContext = function(ctx, drawing, o)
		{
			ctx.begin();
			
			for (var i = 0; i < drawing.ops.length; i++)
			{
				var item = drawing.ops[i];
				var data = item.data;
				
				switch (item.op)
				{
					case 'move':
						ctx.moveTo(data[0], data[1]);
						break;
					case 'bcurveTo':
						ctx.curveTo(data[0], data[1], data[2], data[3], data[4], data[5]);
						break;
					case 'lineTo':
						ctx.lineTo(data[0], data[1]);
						break;
				}
			};
	
			ctx.end();
	
			if (drawing.type === 'fillPath' && o.filled)
			{
				ctx.fill();
			}
			else
			{
				ctx.stroke();
			}
		};
	
		return rc;
	};
	
	/**
	 * Uses RoughJs for drawing comic shapes.
	 */
	(function()
	{
		/**
		 * Adds handJiggle style (jiggle=n sets jiggle)
		 */
		function RoughCanvas(canvas, rc, shape)
		{
			this.canvas = canvas;
			this.rc = rc;
			this.shape = shape;
			
			// Avoids "spikes" in the output
			this.canvas.setLineJoin('round');
			this.canvas.setLineCap('round');
			
			this.originalBegin = this.canvas.begin;
			this.canvas.begin = mxUtils.bind(this, RoughCanvas.prototype.begin);
			
			this.originalEnd = this.canvas.end;
			this.canvas.end = mxUtils.bind(this, RoughCanvas.prototype.end);
					
			this.originalRect = this.canvas.rect;
			this.canvas.rect = mxUtils.bind(this, RoughCanvas.prototype.rect);
	
			this.originalRoundrect = this.canvas.roundrect;
			this.canvas.roundrect = mxUtils.bind(this, RoughCanvas.prototype.roundrect);
			
			this.originalEllipse = this.canvas.ellipse;
			this.canvas.ellipse = mxUtils.bind(this, RoughCanvas.prototype.ellipse);
			
			this.originalLineTo = this.canvas.lineTo;
			this.canvas.lineTo = mxUtils.bind(this, RoughCanvas.prototype.lineTo);
			
			this.originalMoveTo = this.canvas.moveTo;
			this.canvas.moveTo = mxUtils.bind(this, RoughCanvas.prototype.moveTo);
			
			this.originalQuadTo = this.canvas.quadTo;
			this.canvas.quadTo = mxUtils.bind(this, RoughCanvas.prototype.quadTo);
			
			this.originalCurveTo = this.canvas.curveTo;
			this.canvas.curveTo = mxUtils.bind(this, RoughCanvas.prototype.curveTo);
			
			this.originalArcTo = this.canvas.arcTo;
			this.canvas.arcTo = mxUtils.bind(this, RoughCanvas.prototype.arcTo);
			
			this.originalClose = this.canvas.close;
			this.canvas.close = mxUtils.bind(this, RoughCanvas.prototype.close);
			
			this.originalFill = this.canvas.fill;
			this.canvas.fill = mxUtils.bind(this, RoughCanvas.prototype.fill);
			
			this.originalStroke = this.canvas.stroke;
			this.canvas.stroke = mxUtils.bind(this, RoughCanvas.prototype.stroke);
			
			this.originalFillAndStroke = this.canvas.fillAndStroke;
			this.canvas.fillAndStroke = mxUtils.bind(this, RoughCanvas.prototype.fillAndStroke);
			
			this.path = [];
			this.passThrough = false;
		};
	
		RoughCanvas.prototype.moveOp = 'M';
		RoughCanvas.prototype.lineOp = 'L';
		RoughCanvas.prototype.quadOp = 'Q';
		RoughCanvas.prototype.curveOp = 'C';
		RoughCanvas.prototype.closeOp = 'Z';
	
		RoughCanvas.prototype.getStyle = function(stroke, fill)
		{
			// Random seed created from cell ID
			var seed = 1;

			if (this.shape.state != null)
			{
				var str = this.shape.state.cell.id;
				
				if (str != null)
				{
					for (var i = 0; i < str.length; i++)
					{
				    	seed = ((seed << 5) - seed + str.charCodeAt(i)) << 0;
					}
				}
			}

			var style = {strokeWidth: this.canvas.state.strokeWidth, seed: seed, preserveVertices: true};
			var defs = this.rc.getDefaultOptions();
			
			if (stroke)
			{
				style.stroke = this.canvas.state.strokeColor === mxConstants.NONE ?
					'transparent' : this.canvas.state.strokeColor;
			}
			else
			{
				style.stroke = mxConstants.NONE;
			}
			
			var gradient = null;
			style.filled = fill;
			
			if (fill)
			{
				style.fill = this.canvas.state.fillColor === mxConstants.NONE ?
					'' : this.canvas.state.fillColor;
				gradient = this.canvas.state.gradientColor === mxConstants.NONE ?
					null : this.canvas.state.gradientColor;
			}
			else
			{
				style.fill = '';
			}
			
			// Applies cell style
			style['bowing'] = mxUtils.getValue(this.shape.style, 'bowing', defs['bowing']);
			style['hachureAngle'] = mxUtils.getValue(this.shape.style, 'hachureAngle', defs['hachureAngle']);
			style['curveFitting'] = mxUtils.getValue(this.shape.style, 'curveFitting', defs['curveFitting']);
			style['roughness'] = mxUtils.getValue(this.shape.style, 'jiggle', defs['roughness']);
			style['simplification'] = mxUtils.getValue(this.shape.style, 'simplification', defs['simplification']);
			style['disableMultiStroke'] = mxUtils.getValue(this.shape.style, 'disableMultiStroke', defs['disableMultiStroke']);
			style['disableMultiStrokeFill'] = mxUtils.getValue(this.shape.style, 'disableMultiStrokeFill', defs['disableMultiStrokeFill']);
		
			var hachureGap = mxUtils.getValue(this.shape.style, 'hachureGap', -1);
			style['hachureGap'] = (hachureGap == 'auto') ? -1 : hachureGap;
			style['dashGap'] = mxUtils.getValue(this.shape.style, 'dashGap', hachureGap);
			style['dashOffset'] = mxUtils.getValue(this.shape.style, 'dashOffset', hachureGap);
			style['zigzagOffset'] = mxUtils.getValue(this.shape.style, 'zigzagOffset', hachureGap);
			
			var fillWeight = mxUtils.getValue(this.shape.style, 'fillWeight', -1);
			style['fillWeight'] = (fillWeight == 'auto') ? -1 : fillWeight;
			
			var fillStyle = mxUtils.getValue(this.shape.style, 'fillStyle', 'auto');
			
			// Dots fill style is disable due to performance problems
			if (fillStyle == 'dots')
			{
				fillStyle = 'auto';
			}
			
			if (fillStyle == 'auto')
			{
				// One of the following backgrounds for solid fill
				var bg = [mxUtils.hex2rgb('#ffffff')];
				
				if (this.shape.state != null)
				{
					bg.push(mxUtils.hex2rgb(this.shape.state.view.graph.shapeBackgroundColor));
				}

				if (Editor.isDarkMode())
				{
					bg.push(mxUtils.hex2rgb(Editor.darkColor));
				}

				fillStyle = (style.fill != null && (gradient != null || mxUtils.indexOf(
					bg, mxUtils.hex2rgb(style.fill)) >= 0)) ? 'solid' : defs['fillStyle'];
			}

			style['fillStyle'] = fillStyle;
			
			return style;
		};
		
		RoughCanvas.prototype.begin = function()
		{
			if (this.passThrough)
			{
				this.originalBegin.apply(this.canvas, arguments);
			}
			else
			{
				this.path = [];
			}
		};
		
		RoughCanvas.prototype.end = function()
		{
			if (this.passThrough)
			{
				this.originalEnd.apply(this.canvas, arguments);
			}
			else
			{
				// do nothing
			}
		};
		
		RoughCanvas.prototype.addOp = function()
		{
			if (this.path != null)
			{
				this.path.push(arguments[0]);
				
				if (arguments.length > 2)
				{
					var s = this.canvas.state;
		
					for (var i = 2; i < arguments.length; i += 2)
					{
						this.lastX = arguments[i - 1];
						this.lastY = arguments[i];
						
						this.path.push(this.canvas.format((this.lastX)));
						this.path.push(this.canvas.format((this.lastY)));
					}
				}
			}
		};
	
		RoughCanvas.prototype.lineTo = function(endX, endY)
		{
			if (this.passThrough)
			{
				this.originalLineTo.apply(this.canvas, arguments);
			}
			else
			{
				this.addOp(this.lineOp, endX, endY);
				this.lastX = endX;
				this.lastY = endY;
			}
		};
		
		RoughCanvas.prototype.moveTo = function(endX, endY)
		{
			if (this.passThrough)
			{
				this.originalMoveTo.apply(this.canvas, arguments);
			}
			else
			{
				this.addOp(this.moveOp, endX, endY);
				this.lastX = endX;
				this.lastY = endY;
				this.firstX = endX;
				this.firstY = endY;
			}
		};
		
		RoughCanvas.prototype.close = function()
		{
			if (this.passThrough)
			{
				this.originalClose.apply(this.canvas, arguments);
			}
			else
			{
				this.addOp(this.closeOp);
			}
		};
		
		RoughCanvas.prototype.quadTo = function(x1, y1, x2, y2)
		{
			if (this.passThrough)
			{
				this.originalQuadTo.apply(this.canvas, arguments);
			}
			else
			{
				this.addOp(this.quadOp, x1, y1, x2, y2);
				this.lastX = x2;
				this.lastY = y2;
			}
		};
		
		RoughCanvas.prototype.curveTo = function(x1, y1, x2, y2, x3, y3)
		{
			if (this.passThrough)
			{
				this.originalCurveTo.apply(this.canvas, arguments);
			}
			else
			{
				this.addOp(this.curveOp, x1, y1, x2, y2, x3, y3);
				this.lastX = x3;
				this.lastY = y3;
			}
		};
		
		RoughCanvas.prototype.arcTo = function(rx, ry, angle, largeArcFlag, sweepFlag, x, y)
		{
			if (this.passThrough)
			{
				this.originalArcTo.apply(this.canvas, arguments);
			}
			else
			{
				var curves = mxUtils.arcToCurves(this.lastX, this.lastY, rx, ry, angle, largeArcFlag, sweepFlag, x, y);
				
				if (curves != null)
				{
					for (var i = 0; i < curves.length; i += 6) 
					{
						this.curveTo(curves[i], curves[i + 1], curves[i + 2],
							curves[i + 3], curves[i + 4], curves[i + 5]);
					}
				}
				
				this.lastX = x;
				this.lastY = y;
			}
		};
			
		RoughCanvas.prototype.rect = function(x, y, w, h)
		{
			if (this.passThrough)
			{
				this.originalRect.apply(this.canvas, arguments);
			}
			else
			{
				this.path = [];
				this.nextShape = this.rc.generator.rectangle(x, y, w, h, this.getStyle(true, true));
			}
		};
	
		RoughCanvas.prototype.ellipse = function(x, y, w, h)
		{
			if (this.passThrough)
			{
				this.originalEllipse.apply(this.canvas, arguments);
			}
			else
			{
				this.path = [];
				this.nextShape = this.rc.generator.ellipse(x + w / 2, y + h / 2, w, h, this.getStyle(true, true));
			}
		};
			
		RoughCanvas.prototype.roundrect = function(x, y, w, h, dx, dy)
		{
			if (this.passThrough)
			{
				this.originalRoundrect.apply(this.canvas, arguments);
			}
			else
			{
				this.begin();
				this.moveTo(x + dx, y);
				this.lineTo(x + w - dx, y);
				this.quadTo(x + w, y, x + w, y + dy);
				this.lineTo(x + w, y + h - dy);
				this.quadTo(x + w, y + h, x + w - dx, y + h);
				this.lineTo(x + dx, y + h);
				this.quadTo(x, y + h, x, y + h - dy);
				this.lineTo(x, y + dy);
				this.quadTo(x, y, x + dx, y);
			}
		};
	
		RoughCanvas.prototype.drawPath = function(style)
		{
			if (this.path.length > 0)
			{
				this.passThrough = true;
				try
				{
					this.rc.path(this.path.join(' '), style);
				}
				catch (e)
				{
					// ignore
				}
				this.passThrough = false;
			}
			else if (this.nextShape != null)
			{
				for (var key in style)
				{
					this.nextShape.options[key] = style[key];
				}
				
				if (style['stroke'] == mxConstants.NONE ||
					style['stroke'] == null)
				{
					delete this.nextShape.options['stroke'];
				}
				
				if (!style.filled)
				{
					delete this.nextShape.options['fill'];
				}
	
				this.passThrough = true;
				this.rc.draw(this.nextShape);
				this.passThrough = false;
			}	
		};
		
		RoughCanvas.prototype.stroke = function()
		{
			if (this.passThrough)
			{
				this.originalStroke.apply(this.canvas, arguments);
			}
			else
			{
				this.drawPath(this.getStyle(true, false));
			}
		};
		
		RoughCanvas.prototype.fill = function()
		{
			if (this.passThrough)
			{
				this.originalFill.apply(this.canvas, arguments);
			}
			else
			{
				this.drawPath(this.getStyle(false, true));
			}
		};
		
		RoughCanvas.prototype.fillAndStroke = function()
		{
			if (this.passThrough)
			{
				this.originalFillAndStroke.apply(this.canvas, arguments);
			}
			else
			{
				this.drawPath(this.getStyle(true, true));
			}
		};
		
		RoughCanvas.prototype.destroy = function()
		{
			 this.canvas.lineTo = this.originalLineTo;
			 this.canvas.moveTo = this.originalMoveTo;
			 this.canvas.close = this.originalClose;
			 this.canvas.quadTo = this.originalQuadTo;
			 this.canvas.curveTo = this.originalCurveTo;
			 this.canvas.arcTo = this.originalArcTo;
			 this.canvas.close = this.originalClose;
			 this.canvas.fill = this.originalFill;
			 this.canvas.stroke = this.originalStroke;
			 this.canvas.fillAndStroke = this.originalFillAndStroke;
			 this.canvas.begin = this.originalBegin;
			 this.canvas.end = this.originalEnd;
			 this.canvas.rect = this.originalRect;
			 this.canvas.ellipse = this.originalEllipse;
			 this.canvas.roundrect = this.originalRoundrect;
		};
				
		// Returns a new HandJiggle canvas
		mxShape.prototype.createRoughCanvas = function(c)
		{
			return new RoughCanvas(c, Editor.createRoughCanvas(c), this);	
		};
			
		// Overrides to include sketch style
		var shapeCreateHandJiggle = mxShape.prototype.createHandJiggle;
		mxShape.prototype.createHandJiggle = function(c)
		{
			if (!this.outline && this.style != null &&
				mxUtils.getValue(this.style, 'sketch', '0') != '0')
			{
				if (mxUtils.getValue(this.style, 'sketchStyle', 'rough') == 'comic')
				{
					return this.createComicCanvas(c);
				}
				else
				{
					return this.createRoughCanvas(c);	
				}
			}
			else
			{
				return shapeCreateHandJiggle.apply(this, arguments);
			}
		};

		// Avoids duplicate painting of images
		var imageShapePaintVertexShape = mxImageShape.prototype.paintVertexShape;

		mxImageShape.prototype.paintVertexShape = function(c, x, y, w, h)
		{
			if (c.handJiggle == null || !c.handJiggle.passThrough)
			{
				imageShapePaintVertexShape.apply(this, arguments);
			}
		};

		// Overrides for event handling on transparent background for sketch style
		var shapePaint = mxShape.prototype.paint;
		mxShape.prototype.paint = function(c)
		{
			var addTolerance = c.addTolerance;
			var events = true;
			
			if (this.style != null)
			{
				events = mxUtils.getValue(this.style, mxConstants.STYLE_POINTER_EVENTS, '1') == '1';
			}
			
			if (c.handJiggle != null && c.handJiggle.constructor == RoughCanvas && !this.outline)
			{
				// Save needed for possible transforms applied during paint
				c.save();
				var fill = this.fill;
				var stroke = this.stroke;
				this.fill = null;
				this.stroke = null;
				
				var configurePointerEvents = this.configurePointerEvents;
				
				// Ignores color changes during paint
				var setStrokeColor = c.setStrokeColor;
				
				c.setStrokeColor = function()
				{
					// ignore
				};
		
				var setFillColor = c.setFillColor;
				
				c.setFillColor = function()
				{
					// ignore
				};
				
				// Adds stroke tolerance for plain rendering if filled
				if (!events && fill != null)
				{
					this.configurePointerEvents = function()
					{
						// ignore
					};
				}
				
				c.handJiggle.passThrough = true;

				shapePaint.apply(this, arguments);

				c.handJiggle.passThrough = false;
				c.setFillColor = setFillColor;
				c.setStrokeColor = setStrokeColor;
				this.configurePointerEvents = configurePointerEvents;
				this.stroke = stroke;
				this.fill = fill;
				c.restore();
				
				// Bypasses stroke tolerance for sketched rendering if filled
				if (events && fill != null)
				{
					c.addTolerance = function()
					{
						// ignore	
					};
				}
			}
			
			shapePaint.apply(this, arguments);
			c.addTolerance = addTolerance;
		};

		// Overrides glass effect to disable sketch style
		var shapePaintGlassEffect = mxShape.prototype.paintGlassEffect;
		mxShape.prototype.paintGlassEffect = function(c, x, y, w, h, arc)
		{
			if (c.handJiggle != null && c.handJiggle.constructor == RoughCanvas)
			{
				c.handJiggle.passThrough = true;
				shapePaintGlassEffect.apply(this, arguments);
				c.handJiggle.passThrough = false;
			}
			else
			{
				shapePaintGlassEffect.apply(this, arguments);
			}
		};
	})();

	/**
	 * Compresses the given string.
	 */
	Editor.fastCompress = function(data)
	{
		if (data == null || data.length == 0 || typeof(pako) === 'undefined')
		{
			return data;
		}
		else
		{
			return Graph.arrayBufferToString(pako.deflateRaw(data));
		}
	};

	/**
	 * Decompresses the given string.
	 */
	Editor.fastDecompress = function(data)
	{
	   	if (data == null || data.length == 0 || typeof(pako) === 'undefined')
		{
			return data;
		}
		else
		{
			return pako.inflateRaw(Graph.stringToArrayBuffer(atob(data)), {to: 'string'});
		}
	};

	/**
	 * Helper function to extract the graph model XML node.
	 */
	Editor.extractGraphModel = function(node, allowMxFile, checked)
	{
		if (node != null && typeof(pako) !== 'undefined')
		{
			var tmp = node.ownerDocument.getElementsByTagName('div');
			var divs = [];
			
			if (tmp != null && tmp.length > 0)
			{
				for (var i = 0; i < tmp.length; i++)
				{
					if (tmp[i].getAttribute('class') == 'mxgraph')
					{
						divs.push(tmp[i]);
						break;
					}	
				}
			}
			
			if (divs.length > 0)
			{
				var data = divs[0].getAttribute('data-mxgraph');

				if (data != null)
				{
					var config = JSON.parse(data);

					if (config != null && config.xml != null)
					{
						var doc2 = mxUtils.parseXml(config.xml);
						node = doc2.documentElement;
					}
				}
				else
				{
					var divs2 = divs[0].getElementsByTagName('div');
					
					if (divs2.length > 0)
					{
						var data = mxUtils.getTextContent(divs2[0]);
		        		data = Graph.decompress(data, null, checked);
		        		
		        		if (data.length > 0)
		        		{
		        			var doc2 = mxUtils.parseXml(data);
		        			node = doc2.documentElement;
		        		}
					}
				}
			}
		}
		
		if (node != null && node.nodeName == 'svg')
		{
			var tmp = node.getAttribute('content');
			
			if (tmp != null && tmp.charAt(0) != '<' && tmp.charAt(0) != '%')
			{
				tmp = unescape((window.atob) ? atob(tmp) : Base64.decode(cont, tmp));
			}
			
			if (tmp != null && tmp.charAt(0) == '%')
			{
				tmp = decodeURIComponent(tmp);
			}
			
			if (tmp != null && tmp.length > 0)
			{
				node = mxUtils.parseXml(tmp).documentElement;
			}
			else
			{
				throw {message: mxResources.get('notADiagramFile')};
			}
		}
		
		if (node != null && !allowMxFile)
		{
			var diagramNode = null;
			
			if (node.nodeName == 'diagram')
			{
				diagramNode = node;
			}
			else if (node.nodeName == 'mxfile')
			{
				var diagrams = node.getElementsByTagName('diagram');

				if (diagrams.length > 0)
				{
					diagramNode = diagrams[Math.max(0, Math.min(diagrams.length - 1, urlParams['page'] || 0))];
				}
			}
			
			if (diagramNode != null)
			{
				node = Editor.parseDiagramNode(diagramNode, checked);
			}
		}
		
		if (node != null && node.nodeName != 'mxGraphModel' && (!allowMxFile || node.nodeName != 'mxfile'))
		{
			node = null;
		}
		
		return node;
	};
	
	/**
	 * Extracts the XML from the compressed or non-compressed text chunk.
	 */
	Editor.parseDiagramNode = function(diagramNode, checked, allowRecurse)
	{
		Editor.validateDiagramNode(diagramNode);
		var text = mxUtils.trim(mxUtils.getTextContent(diagramNode));
		var node = null;
		
		if (text.length > 0)
		{
			var tmp = Graph.decompress(text, null, checked);
			
			if (tmp != null && tmp.length > 0)
			{
				node = mxUtils.parseXml(tmp).documentElement;
			}
		}
		else
		{
			var temp = mxUtils.getChildNodes(diagramNode);
			
			if (temp.length > 0)
			{
				var tempNode = temp[0];

				if (allowRecurse)
				{
					tempNode = Editor.parseDiagramNode(tempNode, checked, false);
				}

				// Creates new document for unique IDs within mxGraphModel
				var doc = mxUtils.createXmlDocument();
				doc.appendChild(doc.importNode(tempNode, true));
				node = doc.documentElement;
			}
		}
		
		return node;
	};
	
	/**
	 * Extracts the XML from the compressed or non-compressed text chunk.
	 */
	Editor.getDiagramNodeXml = function(diagramNode)
	{
		Editor.validateDiagramNode(diagramNode);
		var text = mxUtils.getNodeValue(diagramNode);
		var xml = null;
		
		if (text.length > 0)
		{
			xml = Graph.decompress(text);
		}
		else
		{
			var temp = diagramNode.getElementsByTagName('mxGraphModel');

			if (temp != null && temp.length > 0)
			{
				xml = mxUtils.getXml(temp[0]);
			}
		}
		
		return xml;
	};

	/**
	 * Initializes diagram nodes with no content.
	 */
	Editor.validateDiagramNode = function(diagramNode)
	{
		if (mxUtils.trim(mxUtils.getTextContent(diagramNode)).length == 0 &&
			mxUtils.getChildNodes(diagramNode).length == 0)
		{
			var codec = new mxCodec();
			var model = new mxGraphModel();
			diagramNode.appendChild(codec.encode(model));
		}

		return diagramNode;
	};

	/**
	 * Static method for parsing PDF files.
	 */
	Editor.extractGraphModelFromPdf = function(base64)
	{
		var result = null;
		base64 = base64.substring(base64.indexOf(',') + 1);

		// Workaround for invalid character error in Safari
		var f = (window.atob && !mxClient.IS_SF) ? atob(base64) : Base64.decode(base64, true);

		// Extracts Subject or Embedded file attachment from PDF 1.7
		if (f.substring(0, 8) == '%PDF-1.7')
		{
			var blockStart = f.indexOf('EmbeddedFile'); 
			
			if (blockStart > -1)
			{
				var streamStart = f.indexOf('stream', blockStart) + 9; //the start of the stream [skipping header check]
				var fileInfo = f.substring(blockStart, streamStart);
				
				if (fileInfo.indexOf('application#2Fvnd.jgraph.mxfile') > 0)
				{
					var streamEnd = f.indexOf('endstream', streamStart - 1);
				
					return pako.inflateRaw(Graph.stringToArrayBuffer(
						f.substring(streamStart, streamEnd)), {to: 'string'});
				}
			}

			var last = f.indexOf('/ObjStm');

			while (last > 0)
			{
				var streamStart = f.indexOf('stream', last) + 9; //the start of the stream [skipping header check]
				var streamEnd = f.indexOf('endstream', streamStart - 1);
				
				function hex_to_ascii(hex)
				{
					var str = [];
					
					for (var n = 0; n < hex.length; n += 2)
					{
						var code = hex.substr(n, 2);

						// Encoded mxfile is URI encoded ASCII
						if (code != '00')
						{
							str.push(String.fromCharCode(parseInt(code, 16)));
						}
					}

					return str.join('');
				};

				var text = pako.inflateRaw(Graph.stringToArrayBuffer(
					f.substring(streamStart, streamEnd)), {to: 'string'});
				var subj = text.indexOf('/Subject <');

				// Extracts Subject from PDF 1.4
				if (subj > 0)
				{
					var temp = text.substring(subj + 14, text.indexOf('>', subj));

					if (temp != null)
					{
						result = hex_to_ascii(temp);
					}

					break;
				}

				last = f.indexOf('/ObjStm', last + 1);
			}
		}

		// Extracts subject from PDF 1.4
		if (result == null && f.substring(0, 8) == '%PDF-1.4')
		{
			var check = '/Subject (%3Cmxfile';
			var curline = '';
			var checked = 0;
			var pos = 0;
			var obj = [];
			var buf = null;
			
			while (pos < f.length)
			{
				var b = f.charCodeAt(pos);
				pos += 1;
				
				if (b != 10)
				{
					curline += String.fromCharCode(b);
				}
				
				if (b == check.charCodeAt(checked))
				{
					checked++;
				}
				else
				{
					checked = 0;
				}
				
				if (checked == check.length)
				{
					var end = f.indexOf('%3C%2Fmxfile%3E', pos) + 15; //15 is the length of encoded </mxfile>
					pos -= 9; //9 is the length of encoded <mxfile

					// Default case is XML inlined in Subject metadata
					if (end > pos)
					{
						result = f.substring(pos, end);

						break;
					}
				}
				
				// Creates table for lookup if no inline data is found
				if (b == 10)
				{
					if (curline == 'endobj')
					{
						buf = null;
					}
					else if (curline.substring(curline.length - 3, curline.length) == 'obj' ||
						curline == 'xref' || curline == 'trailer')
					{
						buf = [];
						obj[curline.split(' ')[0]] = buf;
					}
					else if (buf != null)
					{
						buf.push(curline);
					}
					
					curline = '';
				}
			}
		}
		
		// Extract XML via references
		if (result == null && obj != null)
		{
			result = Editor.extractGraphModelFromXref(obj);
		}
		
		if (result != null)
		{
			result = decodeURIComponent(result.
				replace(/\\\(/g, "(").
				replace(/\\\)/g, ")"));
		}
		
		return result;
	};

	/**
	 * Static method for extracting Subject via references of the form
	 * 
	 * << /Size 33 /Root 20 0 R /Info 1 0 R and 1 0 obj << /Subject 22 0 R
	 * 
	 * Where Info is the metadata block and Subject is the data block.
	 */
	Editor.extractGraphModelFromXref = function(obj)
	{
		var trailer = obj['trailer'];
		var result = null;

		// Gets Info object
		if (trailer != null)
		{
			var arr = /.* \/Info (\d+) (\d+) R/g.exec(trailer.join('\n'));
			
			if (arr != null && arr.length > 0)
			{
				var info = obj[arr[1]];
				
				if (info != null)
				{
					arr = /.* \/Subject (\d+) (\d+) R/g.exec(info.join('\n'));
				
					if (arr != null && arr.length > 0)
					{
						var subj = obj[arr[1]];
						
						if (subj != null)
						{
							subj = subj.join('\n');
							result = subj.substring(1, subj.length - 1);
						}
					}
				}
			}
		}
		
		return result;
	};

	/**
	 * Extracts any parsers errors in the given XML.
	 */
	Editor.extractParserError = function(node, defaultCause)
	{
		var cause = null;
		var errors = (node != null) ? node.getElementsByTagName('parsererror') : null;
		
		if (errors != null && errors.length > 0)
		{
			cause = defaultCause || mxResources.get('invalidChars');
			var divs = errors[0].getElementsByTagName('div');
			
			if (divs.length > 0)
			{
				cause = mxUtils.getTextContent(divs[0]);
			}
		}
		
		return (cause != null) ? mxUtils.trim(cause) : cause;
	};
	
	/**
	 * Adds the given retry function to the given error.
	 */
	Editor.addRetryToError = function(err, retry)
	{
		if (err != null)
		{
			var e = (err.error != null) ? err.error : err;
			
			if (e.retry == null)
			{
				e.retry = retry;
			}
		}
	};
	
	/**
	 * 
	 * Hook for mermaid to draw.io converter.
	 */
	Editor.mermaidToDrawio = function(graph, diagramtype, exta)
	{
		if (typeof mxMermaidToDrawio === 'function')
		{
			return mxMermaidToDrawio(graph, diagramtype, exta);
		}
	};
    
	/**
	 * Global configuration of the Editor
	 * see https://www.drawio.com/doc/faq/configure-diagram-editor
	 * 
	 * For defaultVertexStyle, defaultEdgeStyle and defaultLibraries, this must be called before
	 * mxSettings.load via global config variable window.mxLoadSettings = false.
	 */
	Editor.configure = function(config)
	{
		if (config != null)
		{
			Editor.config = config;
			Editor.configVersion = config.version;

			// Enables debug output
			if (config.debug)
			{
				urlParams['test'] = '1'
			}
			
			if (config.customCss != null)
			{
				var style = document.createElement('style');
				style.type = 'text/css';
				style.appendChild(document.createTextNode(config.customCss));
				document.head.appendChild(style);
			}
			
			if (config.defaultFonts != null)
			{
				Menus.prototype.defaultFonts = config.defaultFonts
			}

			if (config.presetColors != null)
			{
				ColorDialog.prototype.presetColors = config.presetColors
			}

			if (config.defaultColors != null)
			{
				ColorDialog.prototype.defaultColors = config.defaultColors
			}

			if (config.colorNames != null)
			{
				ColorDialog.prototype.colorNames = config.colorNames
			}

			if (config.defaultColorSchemes != null)
			{
				StyleFormatPanel.prototype.defaultColorSchemes = config.defaultColorSchemes
			}

			if (config.defaultEdgeLength != null)
			{
				Graph.prototype.defaultEdgeLength = config.defaultEdgeLength
			}

			if (config.selectParentLayer != null)
			{
				Graph.selectParentLayer = config.selectParentLayer
			}

			if (config.autosaveDelay != null)
			{
				DrawioFile.prototype.autosaveDelay = config.autosaveDelay
			}
			
			if (config.templateFile != null)
			{
				EditorUi.templateFile = config.templateFile;
			}
			
			if (config.styles != null)
			{
				if (Array.isArray(config.styles))
				{
					Editor.styles = config.styles;
				}
				else
				{
					EditorUi.debug('Configuration Error: Array expected for styles');
				}
			}
			
			if (config.globalVars != null)
			{
				Editor.globalVars = config.globalVars;
			}

			if (config.compressXml != null)
			{
				Editor.defaultCompressed = config.compressXml;
				Editor.compressXml = config.compressXml;
			}
			
			if (config.includeDiagram != null)
			{
				Editor.defaultIncludeDiagram = config.includeDiagram;
			}
			
			if (config.simpleLabels != null)
			{
				Editor.simpleLabels = config.simpleLabels;
			}

			if (config.pasteAtMousePointer != null)
			{
				Editor.pasteAtMousePointer = config.pasteAtMousePointer;
			}

			if (config.oneDriveInlinePicker != null)
			{
				Editor.oneDriveInlinePicker = config.oneDriveInlinePicker;
			}

			if (config.defaultAdaptiveColors != null)
			{
				Graph.defaultAdaptiveColors = config.defaultAdaptiveColors;
			}
			else if (config.enableCssDarkMode == false)
			{
				// Backwards compatibility
				Graph.defaultAdaptiveColors = 'simple';
			}

			if (config.enableLightDarkColors != null)
			{
				mxUtils.lightDarkColorSupported = config.enableLightDarkColors;
			}
			
			if (config.darkColor != null)
			{
				Editor.darkColor = config.darkColor;
			}

			if (config.darkColorVar != null)
			{
				Editor.darkColorVar = config.darkColorVar;
			}

			// Updates colors that depend on Editor.darkColor
			// LATER: Add event to update darkColor dependencies
			Graph.prototype.defaultPageBackgroundColor = 'light-dark(#ffffff, ' + Editor.darkColor + ')';
			Graph.prototype.shapeBackgroundColor = 'light-dark(#ffffff, var(' +
				Editor.darkColorVar + ', ' + Editor.darkColor + '))';
			
			if (config.settingsName != null)
			{
				Editor.configurationKey = '.' + config.settingsName + '-configuration';
				Editor.settingsKey = '.' + config.settingsName + '-config';
				mxSettings.key = Editor.settingsKey;
			}
			
			if (config.customFonts != null)
			{
				Menus.prototype.defaultFonts = config.customFonts.
					concat(Menus.prototype.defaultFonts);
			}
			
			if (config.customPresetColors != null)
			{
				ColorDialog.prototype.presetColors = config.customPresetColors.
					concat(ColorDialog.prototype.presetColors);
			}
			
			if (config.customColorSchemes != null)
			{
				StyleFormatPanel.prototype.defaultColorSchemes = config.customColorSchemes.
					concat(StyleFormatPanel.prototype.defaultColorSchemes);
			}
			
			// Custom CSS injected directly into the page
			if (config.css != null)
			{
				var s = document.createElement('style');
				s.setAttribute('type', 'text/css');
				s.appendChild(document.createTextNode(config.css));
				
				var t = document.getElementsByTagName('script')[0];
			  	t.parentNode.insertBefore(s, t);
			}

			if (config.expandLibraries != null)
			{
				Sidebar.prototype.expandLibraries = config.expandLibraries;
			}
			
			if (config.appendCustomLibraries != null)
			{
				Sidebar.prototype.appendCustomLibraries = config.appendCustomLibraries;
			}
			
			// Configures the custom libraries
			if (config.libraries != null)
			{
				Sidebar.prototype.customEntries = config.libraries;
			}
			
			// Defines the enabled built-in libraries.
			if (config.enabledLibraries != null)
			{
				if (Array.isArray(config.enabledLibraries))
				{
					Sidebar.prototype.enabledLibraries = config.enabledLibraries;
				}
				else
				{
					EditorUi.debug('Configuration Error: Array expected for enabledLibraries');
				}
			}
			
			// Overrides default libraries
			if (config.defaultLibraries != null)
			{
				Sidebar.prototype.defaultEntries = config.defaultLibraries;
			}
			
			// Overrides default custom libraries
			if (config.defaultCustomLibraries != null)
			{
				Editor.defaultCustomLibraries = config.defaultCustomLibraries;
			}
			
			// Disables custom libraries
			if (config.enableCustomLibraries != null)
			{
				Editor.enableCustomLibraries = config.enableCustomLibraries;
			}
			
			// Overrides default vertex style
			if (config.defaultVertexStyle != null)
			{
				Graph.prototype.defaultVertexStyle = config.defaultVertexStyle;
			}

			// Overrides default edge style
			if (config.defaultEdgeStyle != null)
			{
				Graph.prototype.defaultEdgeStyle = config.defaultEdgeStyle;
			}

			// Overrides default page visible
			if (config.defaultPageVisible != null)
			{
				Graph.prototype.defaultPageVisible = config.defaultPageVisible;
			}

			// Overrides default grid enabled
			if (config.defaultGridEnabled != null)
			{
				Graph.prototype.defaultGridEnabled = config.defaultGridEnabled;
			}

			// Overrides mouse wheel function
			if (config.zoomWheel != null)
			{
				Graph.zoomWheel = config.zoomWheel;
			}

			// Overrides zoom factor
			if (config.zoomFactor != null)
			{
				var val = parseFloat(config.zoomFactor);
				
				if (!isNaN(val) && val > 1)
				{
					Graph.prototype.zoomFactor = val;
				}
				else
				{
					EditorUi.debug('Configuration Error: Float > 1 expected for zoomFactor');
				}
			}

			// Overrides default grid size
			if (config.defaultGridSize != null)
			{
				var val = parseInt(config.defaultGridSize);
				
				if (!isNaN(val) && val > 0)
				{
					mxGraph.prototype.gridSize = val;
				}
				else
				{
					EditorUi.debug('Configuration Error: Int > 0 expected for defaultGridSize');
				}
			}

			// Overrides grid steps
			if (config.gridSteps != null)
			{
				var val = parseInt(config.gridSteps);
				
				if (!isNaN(val) && val > 0)
				{
					mxGraphView.prototype.gridSteps = val;
				}
				else
				{
					EditorUi.debug('Configuration Error: Int > 0 expected for gridSteps');
				}
			}

			if (config.pageFormat != null)
			{
				var w = parseInt(config.pageFormat.width);
				var h = parseInt(config.pageFormat.height);

				if (!isNaN(w) && w > 0 && !isNaN(h) && h > 0)
				{
					mxGraph.prototype.defaultPageFormat = new mxRectangle(0, 0, w, h);
					mxGraph.prototype.pageFormat = mxGraph.prototype.defaultPageFormat;
				}
				else
				{
					EditorUi.debug('Configuration Error: {width: int, height: int} expected for pageFormat');
				}
			}
			
			if (config.thumbWidth != null)
			{
				Sidebar.prototype.thumbWidth = config.thumbWidth;
			}
			
			if (config.thumbHeight != null)
			{
				Sidebar.prototype.thumbHeight = config.thumbHeight;
			}
			
			if (config.emptyLibraryXml != null)
			{
				EditorUi.prototype.emptyLibraryXml = config.emptyLibraryXml;
			}

			if (config.emptyDiagramXml != null)
			{
				EditorUi.prototype.emptyDiagramXml = config.emptyDiagramXml;
			}
			
			if (config.sidebarWidth != null)
			{
				EditorUi.prototype.hsplitPosition = config.sidebarWidth;
			}

			if (config.updateDefaultStyle != null)
			{
				EditorUi.prototype.updateDefaultStyle = config.updateDefaultStyle;
			}
			
			if (config.sidebarTitles != null)
			{
				Sidebar.prototype.sidebarTitles = config.sidebarTitles;
			}
			
			if (config.sidebarTitleSize != null)
			{
				var val = parseInt(config.sidebarTitleSize);
				
				if (!isNaN(val) && val > 0)
				{
					Sidebar.prototype.sidebarTitleSize = val;
				}
				else
				{
					EditorUi.debug('Configuration Error: Int > 0 expected for sidebarTitleSize');
				}
			}
			
			if (config.fontCss != null)
			{
				if (typeof config.fontCss === 'string')
				{
					Editor.configureFontCss(config.fontCss);
				}
				else
				{
					EditorUi.debug('Configuration Error: String expected for fontCss');
				}
			}
			
			if (config.autosaveDelay != null)
			{
				var val = parseInt(config.autosaveDelay);
				
				if (!isNaN(val) && val > 0)
				{
					DrawioFile.prototype.autosaveDelay = val;
				}
				else
				{
					EditorUi.debug('Configuration Error: Int > 0 expected for autosaveDelay');
				}
			}

			if(config.maxImageBytes != null) 
			{
				EditorUi.prototype.maxImageBytes = config.maxImageBytes;
			}
			
			if(config.maxImageSize != null) 
			{
				EditorUi.prototype.maxImageSize = config.maxImageSize;
			}
			
			if (config.shareCursorPosition != null)
			{
				EditorUi.prototype.shareCursorPosition = config.shareCursorPosition;
			}

			if (config.showRemoteCursors != null)
			{
				EditorUi.prototype.showRemoteCursors = config.showRemoteCursors;
			}

			if (config.restrictExport != null)
			{
				DrawioFile.RESTRICT_EXPORT = config.restrictExport;
			}
			
			if (config.replaceSvgDataUris != null)
			{
				Editor.replaceSvgDataUris = config.replaceSvgDataUris;
			}

			if (config.foreignObjectImages != null)
			{
				Editor.foreignObjectImages = config.foreignObjectImages;
			}

			if (config.shadowColor != null)
			{
				mxConstants.SHADOW_COLOR = config.shadowColor;
			}

			if (config.shadowOpacity != null)
			{
				mxConstants.SHADOW_OPACITY = config.shadowOpacity;
			}

			if (config.shadowOffsetX != null)
			{
				mxConstants.SHADOW_OFFSET_X = config.shadowOffsetX;
			}

			if (config.shadowOffsetY != null)
			{
				mxConstants.SHADOW_OFFSET_Y = config.shadowOffsetY;
			}

			if (config.shadowBlur != null)
			{
				mxConstants.SHADOW_BLUR = config.shadowBlur;
			}

			if (config.enableAnimations != null)
			{
				Editor.enableAnimations = config.enableAnimations;
			}
			
			if (config.enableChatGpt != null)
			{
				Editor.enableChatGpt = config.enableChatGpt;
			}

			if (config.gptApiKey != null)
			{
				Editor.gptApiKey = config.gptApiKey;
			}

			if (config.gptModel != null)
			{
				Editor.gptModel = config.gptModel;
			}

			if (config.gptUrl != null)
			{
				Editor.gptUrl = config.gptUrl;
			}
		}
	};
	
	/**
	 * 
	 */
	Editor.isSettingsEnabled = function()
	{
		return typeof window.mxSettings !== 'undefined' && (isLocalStorage || mxClient.IS_CHROMEAPP);
	};
	
	/**
	 * Adds the global fontCss configuration.
	 */
	Editor.configureFontCss = function(fontCss)
	{
		if (fontCss != null)
		{
			Editor.prototype.fontCss = fontCss;
			var t = document.getElementsByTagName('script')[0];
			
			if (t != null && t.parentNode != null)
			{
				var s = document.createElement('style');
				s.setAttribute('type', 'text/css');
				s.appendChild(document.createTextNode(fontCss));
				t.parentNode.insertBefore(s, t);
				
				// Preloads fonts where supported
				var parts = fontCss.split('url(');
				
				for (var i = 1; i < parts.length; i++)
				{
				    var idx = parts[i].indexOf(')');
				    var url = Editor.trimCssUrl(parts[i].substring(0, idx));
				    
				    var l = document.createElement('link');
					l.setAttribute('rel', 'preload');
					l.setAttribute('href', url);
					l.setAttribute('as', 'font');
					l.setAttribute('crossorigin', '');
					
				  	t.parentNode.insertBefore(l, t);
				}
			}
		}
	};
			
	/**
	 * Strips leading and trailing quotes and spaces
	 */
    Editor.trimCssUrl = function(str)
    {
    	return str.replace(new RegExp("^[\\s\"']+", "g"), "").replace(new RegExp("[\\s\"']+$", "g"), "");
    }
    
    /**
     * Prefix for URLs that reference Google fonts.
     */
	Editor.GOOGLE_FONTS = 'https://fonts.googleapis.com/css?family=';
     
    /**
     * Prefix for URLs that reference Google fonts with CSS2.
     */
	Editor.GOOGLE_FONTS_CSS2 = 'https://fonts.googleapis.com/css2?family=';
    
	/**
	 * Alphabet for global unique IDs.
	 */
	Editor.GUID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_';

	/**
	 * Default length for global unique IDs.
	 */
	Editor.GUID_LENGTH = 20;
	
	/**
	 * Default length for global unique IDs.
	 */
	Editor.guid = function(length)
	{
		var len = (length != null) ? length : Editor.GUID_LENGTH;
		var rtn = [];
	  
		for (var i = 0; i < len; i++)
		{
			rtn.push(Editor.GUID_ALPHABET.charAt(Math.floor(Math.random() * Editor.GUID_ALPHABET.length)));
		}

		return rtn.join('');
	};

	/**
	 * Interval for updating the file status.
	 */
	Editor.updateStatusInterval = 10000;

	/**
	 * Specifies the app name. Default is document.title.
	 */
	Editor.prototype.appName = 'draw.io';
		
	/**
	 * Known file types.
	 */
	Editor.prototype.diagramFileTypes = [
		{description: 'diagramXmlDesc', extension: 'drawio', mimeType: 'text/xml'},
		{description: 'diagramPngDesc', extension: 'png', mimeType: 'image/png'},
		{description: 'diagramSvgDesc', extension: 'svg', mimeType: 'image/svg'},
		{description: 'diagramHtmlDesc', extension: 'html', mimeType: 'text/html'}];
	
	/**
	 * Known file types.
	 */
	Editor.prototype.libraryFileTypes = [{description: 'Library (.drawiolib, .xml)', extensions: ['drawiolib', 'xml']}];
 
	/**
	 * Additional help text for special file extensions.
	 */
	Editor.prototype.fileExtensions = [
		{ext: 'html', title: 'filetypeHtml'},
		{ext: 'png', title: 'filetypePng'},
		{ext: 'svg', title: 'filetypeSvg'}];
	
	/**
	 * General timeout is 25 seconds.
	 */
	Editor.prototype.timeout = 25000;
	
	/**
	 * Executes the first step for connecting to Google Drive.
	 */
	Editor.prototype.editButtonLink = (urlParams['edit'] != null) ? decodeURIComponent(urlParams['edit']) : null;

	/**
	 * Specifies if img.crossOrigin is supported. This is true for all browsers except IE10 and earlier.
	 */
	Editor.prototype.crossOriginImages = !mxClient.IS_IE;
	
	/**
	 * Adds support for old stylesheets and compressed files
	 */
	var editorSetGraphXml = Editor.prototype.setGraphXml;
	Editor.prototype.setGraphXml = function(node)
	{
		node = (node != null && node.nodeName != 'mxlibrary') ? this.extractGraphModel(node) : null;

		if (node != null)
		{
			// Checks for parser errors
			var cause = Editor.extractParserError(node, mxResources.get('invalidOrMissingFile'));

			if (cause)
			{
				EditorUi.debug('Editor.setGraphXml ParserError', [this],
					'node', [node], 'cause', [cause]);

				throw new Error(mxResources.get('notADiagramFile') + ' (' + cause + ')');
			}
			else if (node.nodeName == 'mxGraphModel')
			{
				var style = node.getAttribute('style') || 'default-style2';
				
				// Decodes the style if required
				if (urlParams['embed'] != '1' && (style == null || style == ''))
				{
					var node2 = (this.graph.themes != null) ?
						this.graph.themes['default-old'] :
						mxUtils.load(STYLE_PATH + '/default-old.xml').getDocumentElement();
				    
				    if (node2 != null)
				    {
				    	var dec2 = new mxCodec(node2.ownerDocument);
				    	dec2.decode(node2, this.graph.getStylesheet());
				    }
				}
				else if (style != this.graph.currentStyle)
				{
				    var node2 = (this.graph.themes != null) ?
						this.graph.themes[style] :
						mxUtils.load(STYLE_PATH + '/' + style + '.xml').getDocumentElement()
				    
				    if (node2 != null)
				    {
				    	var dec2 = new mxCodec(node2.ownerDocument);
				    	dec2.decode(node2, this.graph.getStylesheet());
				    }
				}
	
				this.graph.currentStyle = style;
				this.graph.mathEnabled = (urlParams['math'] == '1' || node.getAttribute('math') == '1');
				this.graph.setAdaptiveColors(node.getAttribute('adaptiveColors'));
				
				var bgImg = node.getAttribute('backgroundImage');
				
				if (bgImg != null)
				{
					this.graph.setBackgroundImage(this.graph.parseBackgroundImage(bgImg));
				}
				else
				{
					this.graph.setBackgroundImage(null);
				}
				
				this.graph.useCssTransforms = !mxClient.NO_FO &&
					this.isChromelessView() &&
					this.graph.isCssTransformsSupported();
				this.graph.updateCssTransform();

				this.graph.setShadowVisible(node.getAttribute('shadow') == '1', false);
				
				var extFonts = node.getAttribute('extFonts');
				
				if (extFonts)
				{
					try
					{
						extFonts = extFonts.split('|').map(function(ef)
						{
							var parts = ef.split('^');
							return {name: parts[0], url: parts[1]};
						});
						
						for (var i = 0; i < extFonts.length; i++)
						{
							this.graph.addExtFont(extFonts[i].name, extFonts[i].url);
						}
					}
					catch(e)
					{
						console.log('ExtFonts format error: ' + e.message);
					}
				}
				else if (this.graph.extFonts != null && this.graph.extFonts.length > 0)
				{
					this.graph.extFonts = [];
				}
			}
	
			// Calls updateGraphComponents
			editorSetGraphXml.apply(this, arguments);
		}
		else
		{
			throw { 
			    message: mxResources.get('notADiagramFile') || 'Invalid data',
			    toString: function() { return this.message; }
			};
		}
	};

	/**
	 * Adds persistent style to file
	 */
	var editorGetGraphXml = Editor.prototype.getGraphXml;	
	Editor.prototype.getGraphXml = function(ignoreSelection, resolveReferences)
	{
		ignoreSelection = (ignoreSelection != null) ? ignoreSelection : true;
		var node = editorGetGraphXml.apply(this, arguments);
		
		// Adds the current style
		if (this.graph.currentStyle != null && this.graph.currentStyle != 'default-style2')
		{
			node.setAttribute('style', this.graph.currentStyle);
		}

		var bgImg = this.graph.getBackgroundImageObject(
			this.graph.backgroundImage,
			resolveReferences);
		
		// Adds the background image
		if (bgImg != null)
		{
			node.setAttribute('backgroundImage', JSON.stringify(bgImg));
		}
		
		node.setAttribute('math', (this.graph.mathEnabled) ? '1' : '0');
		node.setAttribute('shadow', (this.graph.shadowVisible) ? '1' : '0');

		if (this.graph.adaptiveColors != null)
		{
			node.setAttribute('adaptiveColors', this.graph.adaptiveColors);
		}
		else
		{
			node.removeAttribute('adaptiveColors');
		}
		
		if (this.graph.extFonts != null && this.graph.extFonts.length > 0)
		{
			var strExtFonts = this.graph.extFonts.map(function(ef)
			{
				return ef.name + '^' + ef.url;
			});
			
			node.setAttribute('extFonts', strExtFonts.join('|'));
		}
		
		return node;
	};
	
	/**
	 * Helper function to extract the graph model XML node.
	 */
	Editor.prototype.isDataSvg = function(svg)
	{
		try
		{
			var svgRoot = mxUtils.parseXml(svg).documentElement;
			var tmp = svgRoot.getAttribute('content');
			
			if (tmp != null)
			{
				if (tmp != null && tmp.charAt(0) != '<' && tmp.charAt(0) != '%')
				{
					tmp = unescape((window.atob) ? atob(tmp) : Base64.decode(cont, tmp));
				}
				
				if (tmp != null && tmp.charAt(0) == '%')
				{
					tmp = decodeURIComponent(tmp);
				}
				
				if (tmp != null && tmp.length > 0)
				{
					var node = mxUtils.parseXml(tmp).documentElement;
					
					return node.nodeName == 'mxfile' || node.nodeName == 'mxGraphModel';
				}
			}
		}
		catch (e)
		{
			// ignore
		}
		
		return false;
	};
	
	/**
	 * Helper function to extract the graph model XML node.
	 */
	Editor.prototype.extractGraphModel = function(node, allowMxFile, checked)
	{
		return Editor.extractGraphModel.apply(this, arguments);
	};
	
	/**
	 * Overrides reset graph.
	 */
	var editorResetGraph = Editor.prototype.resetGraph;	
	Editor.prototype.resetGraph = function()
	{
		this.graph.mathEnabled = (urlParams['math'] == '1');
		this.graph.setAdaptiveColors(null);
		this.graph.view.x0 = null;
		this.graph.view.y0 = null;
		
		this.graph.useCssTransforms = !mxClient.NO_FO &&
			this.isChromelessView() &&
			this.graph.isCssTransformsSupported();
		this.graph.updateCssTransform();
		
		editorResetGraph.apply(this, arguments);
	};

	/**
	 * Math support.
	 */
	var editorUpdateGraphComponents = Editor.prototype.updateGraphComponents;
	Editor.prototype.updateGraphComponents = function()
	{
		editorUpdateGraphComponents.apply(this, arguments);
		
		this.graph.useCssTransforms = !mxClient.NO_FO &&
			this.isChromelessView() &&
			this.graph.isCssTransformsSupported();
		this.graph.updateCssTransform();
	};
	
	/**
	 * Initializes math typesetting and loads respective code.
	 */
	Editor.initMath = function(src, config)
	{
		if (typeof window.MathJax === 'undefined' && !mxClient.IS_IE && !mxClient.IS_IE11)
		{
			src = (src != null) ? src : DRAW_MATH_URL + '/startup.js';
			Editor.mathJaxQueue = [];

			// Blocks concurrent rendering while
			// async rendering is in progress
			var rendering = null;

			function mathJaxDone()
			{
				rendering = null;

				if (Editor.mathJaxQueue.length > 0)
				{
					Editor.doMathJaxRender(Editor.mathJaxQueue.shift());
				}
				else
				{
					Editor.onMathJaxDone();
				}
			};
			
			Editor.doMathJaxRender = function(container)
			{
				try
				{
					if (rendering == null)
					{
						MathJax.typesetClear([container]);
						MathJax.typeset([container]);
						mathJaxDone();
					}
					else if (rendering != container)
					{
						Editor.mathJaxQueue.push(container);
					}
				}
				catch (e)
				{
					MathJax.typesetClear([container]);

					if (e.retry != null)
					{
						rendering = container;

						e.retry.then(function()
						{
							MathJax.typesetPromise([container]).then(mathJaxDone)['catch'](function(e)
							{
								console.log('Error in MathJax.typesetPromise: ' + e.toString());
								mathJaxDone();
							});
						})['catch'](function(e)
						{
							console.log('Error in MathJax.retry: ' + e.toString());
							mathJaxDone();
						});;
					}
					else if (window.console != null)
					{
						console.log('Error in MathJax.typeset: ' + e.toString());
					}
				}
			};
			
			window.MathJax = (config != null) ? config :
			{
				options:
				{
					skipHtmlTags: {'[+]': ['text']},
					ignoreHtmlClass: 'geDisableMathJax'
				},
				loader:
				{
					load: [(urlParams['math-output'] == 'html') ?
						'output/chtml' : 'output/svg', 'input/tex',
						'input/asciimath', 'ui/safe']
				},
				startup:
				{
					pageReady: function()
					{
						for (var i = 0; i < Editor.mathJaxQueue.length; i++)	
						{	
							Editor.doMathJaxRender(Editor.mathJaxQueue[i]);	
						}
					}
				}
			};

			// Adds global enqueue method for async rendering
			Editor.MathJaxRender = function(container)
			{
				if (typeof MathJax !== 'undefined' && typeof MathJax.typeset === 'function')
				{
					Editor.doMathJaxRender(container);
				}
				else
				{
					Editor.mathJaxQueue.push(container);
				}
			};
			
			// Adds global MathJax render callback
			Editor.onMathJaxDone = function()
			{
				// Hook for listeners
			};

			// Updates math typesetting after changes
			var editorInit = Editor.prototype.init;
			
			Editor.prototype.init = function()
			{
				editorInit.apply(this, arguments);

				var renderMath = mxUtils.bind(this, function(sender, evt)
				{
					if (this.graph.container != null &&
						this.graph.mathEnabled)
					{
						Editor.MathJaxRender(this.graph.container);
					}
				});
				
				this.graph.model.addListener(mxEvent.CHANGE, renderMath);
				this.graph.addListener(mxEvent.REFRESH, renderMath);
			};
			
			var tags = document.getElementsByTagName('script');
			
			if (tags != null && tags.length > 0)
			{
				var s = document.createElement('script');
				s.setAttribute('type', 'text/javascript');
				s.setAttribute('src', src);
				tags[0].parentNode.appendChild(s);
			}
		}
	};

	/**
	 * Parses line of CSV values according to RFC 4180.
	 */
	Editor.prototype.csvToArray = function(text)
	{
		if (text.length > 0)
		{
			var p = '', row = [''], i = 0, s = !0, l;

			for (l of text)
			{
				if ('"' === l)
				{
					if (s && l === p)
					{
						row[i] += l;
					}

					s = !s;
				}
				else if (',' === l && s)
				{
					l = row[++i] = '';
				}
				else
				{
					row[i] += l;
				}

				p = l;
			}
			
			return row;
		}
		else
		{
			return [];
		}
	};

	/**
	 * Returns an URL that is proxied if the given URL is blocked. Uses
	 * direct URL if no CSP is used as proxy blocks unknown text content.
	 */
	Editor.prototype.getProxiedUrl = function(url)
	{
		if ((/test\.draw\.io$/.test(window.location.hostname) ||
			/app\.diagrams\.net$/.test(window.location.hostname)) &&
			!this.isCorsEnabledForUrl(url))
		{
			var isVisioFilename = EditorUi.isVisioFilename(url);
			var binary = /\.png$/i.test(url) || /\.pdf$/i.test(url);
			var base64 = binary || isVisioFilename;
			var nocache = 't=' + new Date().getTime();
			url = PROXY_URL + '?url=' + encodeURIComponent(url) +
				'&' + nocache + ((base64) ? '&base64=1' : '');
		}

		return url;
	};

	/**
	 * Returns true if the given URL is known to have CORS headers and is
	 * allowed by CSP.
	 */
	Editor.prototype.isCorsEnabledForUrl = function(url)
	{
		// Disables proxy for desktop and chrome app as it is served locally
		if (mxClient.IS_CHROMEAPP || EditorUi.isElectronApp)
		{
			return true;
		}
		
		// Does not use proxy for same domain
		if (url.substring(0, window.location.origin.length) == window.location.origin)
		{
			return true;
		}

		// Blocked by CSP in production but allowed for hosted deployment
		if (urlParams['cors'] != null && this.corsRegExp == null)
		{
			this.corsRegExp = new RegExp(decodeURIComponent(urlParams['cors']));
		}
		
		// No access-control-allow-origin for some Iconfinder images, add this when fixed:
		// /^https?:\/\/[^\/]*\.iconfinder.com\//.test(url) ||
		return (this.corsRegExp != null && this.corsRegExp.test(url)) ||
			url.substring(0, 34) === 'https://raw.githubusercontent.com/' ||
			url.substring(0, 29) === 'https://fonts.googleapis.com/' ||
			url.substring(0, 26) === 'https://fonts.gstatic.com/';
	};

	/**
	 * Converts all images in the SVG output to data URIs for immediate rendering
	 */
	Editor.prototype.createImageUrlConverter = function()
	{
		var converter = new mxUrlConverter();
		converter.updateBaseUrl();

		// Extends convert to avoid CORS using an image proxy server where needed
		var convert = converter.convert;
		var self = this;
		
		converter.convert = function(src)
		{
			if (src != null && navigator.onLine)
			{
				var remote = src.substring(0, 7) == 'http://' || src.substring(0, 8) == 'https://';
				
				if (remote && src.substring(0, converter.baseUrl.length) != converter.baseUrl &&
						(!self.crossOriginImages || !self.isCorsEnabledForUrl(src)))
				{
					src = PROXY_URL + '?url=' + encodeURIComponent(src);
				}
				else if (src.substring(0, 19) != 'chrome-extension://')
				{
					src = convert.apply(this, arguments);
				}
			}
			
			return src;
		};
		
		return converter;
	};

	/**
	 * 
	 */
	Editor.createSvgDataUri = function(svg)
	{
		return 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svg)));
	};

	/**
	 * 
	 */
	Editor.prototype.convertImageToDataUri = function(url, callback, error, convertScale, forceConvert)
	{
		try
		{
			var acceptResponse = true;
			
			var timeoutThread = window.setTimeout(mxUtils.bind(this, function()
			{
				acceptResponse = false;
				callback(url);
			}), this.timeout);

			// Fallback to raster image if SVG cannot be loaded
			var svgError = mxUtils.bind(this, function()
			{
				if (convertScale != null)
				{
					this.convertImageToDataUri(url, callback, error, convertScale, true);
				}
				else
				{
					callback(url);
				}
			});
	
			if (/(\.svg)$/i.test(url) && !forceConvert)
			{
				mxUtils.get(url, mxUtils.bind(this, function(req)
				{
			    	window.clearTimeout(timeoutThread);

					if (acceptResponse)
					{
						if (req.getStatus() < 200 || req.getStatus() > 299)
						{
							svgError();
						}
						else
						{
							callback(Editor.createSvgDataUri(req.getText()));
						}

						
					}
				}),
				mxUtils.bind(this, function()
				{
			    	window.clearTimeout(timeoutThread);

					if (acceptResponse)
					{
						svgError();
					}
				}));
			}
			else
			{
			    var img = new Image();
			    
			    if (this.crossOriginImages)
		    	{
				    img.crossOrigin = 'anonymous';
			    }
			    
			    img.onload = function()
			    {
			    	window.clearTimeout(timeoutThread);
					
					if (acceptResponse)
					{
				        try
				        {
							convertScale = (convertScale != null &&
								forceConvert) ? convertScale : 1;

					        var canvas = document.createElement('canvas');
					        var ctx = canvas.getContext('2d');
							ctx.scale(convertScale, convertScale);
					        canvas.height = img.height * convertScale;
					        canvas.width = img.width * convertScale;
					        ctx.drawImage(img, 0, 0);
							
				        	callback(canvas.toDataURL());
				        }
				        catch (e)
				        {
			        		callback(url);
				        }
					}
			    };
			    
			    img.onerror = function()
			    {
			    	window.clearTimeout(timeoutThread);
					
					if (acceptResponse)
					{
						if (error != null)
						{
							error();
						}
						else
						{
							callback(url);
						}
					}
			    };
			    
			    img.src = url;
			}
		}
		catch (e)
		{
			if (error != null)
			{
				error();
			}
			else
			{
				callback(url);
			}
		}
	};
	
	/**
	 * Converts all images in the SVG output to data URIs for immediate rendering
	 */
	Editor.prototype.convertImages = function(svgRoot, callback, imageCache, converter)
	{
		// Converts images to data URLs for immediate painting
		if (converter == null)
		{
			converter = this.createImageUrlConverter();
		}
		
		// Queues image conversion and executes in order
		var pending = [];

		function next()
		{
			if (pending.length > 0)
			{
				pending.shift()();
			}
			else
			{
				callback(svgRoot);
			}
		};

		var cache = imageCache || new Object();
		
		var convertImages = mxUtils.bind(this, function(tagName, srcAttr)
		{
			var images = svgRoot.getElementsByTagName(tagName);
			
			for (var i = 0; i < images.length; i++)
			{
				(mxUtils.bind(this, function(img)
				{
					pending.push(mxUtils.bind(this, function()
					{
						try
						{
							if (img != null)
							{
								var src = converter.convert(img.getAttribute(srcAttr));

								// Data URIs are pass-through
								if (src != null && src.substring(0, 5) != 'data:')
								{
									var tmp = cache[src];

									if (tmp == null)
									{
										this.convertImageToDataUri(src, function(uri)
										{
											if (uri != null)
											{
												cache[src] = uri;
												img.setAttribute(srcAttr, uri);
											}
											
											next();
										}, null, Editor.svgRasterScale);
									}
									else
									{
										img.setAttribute(srcAttr, tmp);

										next();
									}
								}
								else
								{
									if (src != null)
									{
										img.setAttribute(srcAttr, src);
									}

									next();
								}
							}
						}
						catch (e)
						{
							next();
						}
					}));
				}))(images[i]);
			}
		});
		
		// Converts all known image tags in output
		// LATER: Add support for images in CSS
		convertImages('image', 'xlink:href');
		convertImages('img', 'src');
		next();
	};
		
	/**
	 * Base64 encodes the given string. This method seems to be more
	 * robust for encoding PNG from binary AJAX responses.
	 */
	Editor.base64Encode = function(str)
	{
	    var CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
	    var out = "", i = 0, len = str.length, c1, c2, c3;
	    
	    while (i < len)
	    {
	        c1 = str.charCodeAt(i++) & 0xff;
	        
	        if (i == len)
	        {
	            out += CHARS.charAt(c1 >> 2);
	            out += CHARS.charAt((c1 & 0x3) << 4);
	            out += "==";
	            break;
	        }
	        
	        c2 = str.charCodeAt(i++);
	        
	        if (i == len)
	        {
	            out += CHARS.charAt(c1 >> 2);
	            out += CHARS.charAt(((c1 & 0x3)<< 4) | ((c2 & 0xF0) >> 4));
	            out += CHARS.charAt((c2 & 0xF) << 2);
	            out += "=";
	            break;
	        }
	        
	        c3 = str.charCodeAt(i++);
	        out += CHARS.charAt(c1 >> 2);
	        out += CHARS.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4));
	        out += CHARS.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6));
	        out += CHARS.charAt(c3 & 0x3F);
	    }
	    
	    return out;
	};

	/**
	 * Checks if the client is authorized and calls the next step.
	 */
	Editor.prototype.loadUrl = function(url, success, error, forceBinary, retry, dataUriPrefix, noBinary, headers)
	{
		try
		{
			var binary = !noBinary && (forceBinary || /(\.png)($|\?)/i.test(url) ||
				/(\.jpe?g)($|\?)/i.test(url) || /(\.gif)($|\?)/i.test(url) ||
				/(\.pdf)($|\?)/i.test(url));
			retry = (retry != null) ? retry : true;
			
			var fn = mxUtils.bind(this, function()
			{
				mxUtils.get(url, mxUtils.bind(this, function(req)
				{
					if (req.getStatus() >= 200 && req.getStatus() <= 299)
					{
				    	if (success != null)
				    	{
					    	var data = req.getText();
					    	
				    		// Returns PNG as base64 encoded data URI
							if (binary)
							{
								// LATER: Could be JPG but modern browsers
								// ignore the mime type in the data URI
								dataUriPrefix = (dataUriPrefix != null) ?
									dataUriPrefix : 'data:image/png;base64,';
								data = dataUriPrefix + Editor.base64Encode(data);
							}
							
				    		success(data);
				    	}
					}
					else if (error != null)
			    	{
						if (req.getStatus() == 0)
						{
							// Handles CORS errors
							error({message: mxResources.get('accessDenied')}, req);
						}
						else if (req.getStatus() == 404)
						{
							error({message: mxResources.get('fileNotFound'),
								code: req.getStatus()}, req);
						}
						else
						{
							error({message: this.getErrorMessage(req)}, req);
						}
			    	}
				}), function(req)
				{
			    	if (error != null)
			    	{
			    		error({message: mxResources.get('error') + ' ' + req.getStatus()});
			    	}
				}, binary, this.timeout, function()
			    {
				    if (retry && error != null)
					{
						error({code: App.ERROR_TIMEOUT, retry: fn});
					}
			    }, headers);
			});
			
			fn();
		}
		catch (e)
		{
			if (error != null)
			{
				error(e);
			}
		}
	};

	/**
	 * Adds the listener for automatically saving the diagram for local changes.
	 */
	Editor.prototype.getErrorMessage = function(req)
	{
		var msg = mxResources.get('error') + ' ' + req.getStatus();

		try
		{
			var data = req.getText();
			var obj = JSON.parse(data);

			if (obj != null && obj.error != null &&
				obj.error.message != null)
			{
				msg = obj.error.message + ' (' + msg + ')';
			}
		}
		catch (e)
		{
			// ignore
		}

		return msg;
	};

	/**
	 * Makes all relative font URLs absolute in the given font CSS.
	 */
    Editor.prototype.absoluteCssFonts = function(fontCss)
    {
    	var result = null;
    	
    	if (fontCss != null)
    	{
    		var parts = fontCss.split('url(');
    		
    		if (parts.length > 0)
    		{
    			result = [parts[0]];
    		
    			// Gets path for URL
    			var path = window.location.pathname;
    			var idx = (path != null) ? path.lastIndexOf('/') : -1;
    			
    			if (idx >= 0)
    			{
    				path = path.substring(0, idx + 1);
    			}
    			
    			// Gets base tag from head
    			var temp = document.getElementsByTagName('base');
    			var base = null;
    			
    			if (temp != null && temp.length > 0)
    			{
    				base = temp[0].getAttribute('href');
    			}
    		
    			for (var i = 1; i < parts.length; i++)
    			{
    				var idx = parts[i].indexOf(')');
    				
    				if (idx > 0)
    				{
	    				var url = Editor.trimCssUrl(parts[i].substring(0, idx));
	    				
	    				if (this.graph.isRelativeUrl(url))
	    				{
	                        url = (base != null) ? base + url : (window.location.protocol + '//' + window.location.hostname +
	                        	((url.charAt(0) == '/') ? '' : path) + url);
	                    }
	    				
	    				result.push('url("' + url + '"' + parts[i].substring(idx));
    				}
    				else
    				{
    					result.push(parts[i]);
    				}
    			}
    		}
    		else
    		{
    			result = [fontCss]
    		}
    	}
    	
    	return (result != null) ? result.join('') : null;
	};
	
	/**
	 * Returns the URL and mime type to be used for the given font.
	 */
	Editor.prototype.mapFontUrl = function(mime, url, fn)
	{
		if ((/^https?:\/\//.test(url)) && !this.isCorsEnabledForUrl(url))
		{
			url = PROXY_URL + '?url=' + encodeURIComponent(url);
		}

		fn(mime, url);
	};

	/**
	 * For the fonts in CSS to be applied when rendering images on canvas, the actual
	 * font data must be made available via a data URI encoding of the file.
	 */
    Editor.prototype.embedCssFonts = function(fontCss, then)
    {
        var parts = fontCss.split('url(');
        var waiting = 0;
		
        if (this.cachedFonts == null) 
        {
        	this.cachedFonts = {};
        }

        var finish = mxUtils.bind(this, function()
        {
            if (waiting == 0)
            {
                // Constructs string
                var result = [parts[0]];
                
                for (var j = 1; j < parts.length; j++)
                {
                    var idx = parts[j].indexOf(')');
                    result.push('url("');
                    result.push(this.cachedFonts[Editor.trimCssUrl(parts[j].substring(0, idx))]);
                    result.push('"' + parts[j].substring(idx));
                }
				
                then(result.join(''));
            }
        });
        
        if (parts.length > 0)
        {
            for (var i = 1; i < parts.length; i++)
            {
                var idx = parts[i].indexOf(')');
                var format = null;
                
                // Checks if there is a format directive
                var fmtIdx = parts[i].indexOf('format(', idx);
                
                if (fmtIdx > 0)
                {
                    format = Editor.trimCssUrl(parts[i].substring(fmtIdx + 7, parts[i].indexOf(')', fmtIdx)));
                }

                (mxUtils.bind(this, function(url)
                {
                    if (this.cachedFonts[url] == null)
                    {
                        // Mark font as being fetched and fetch it
                    	this.cachedFonts[url] = url;
                        waiting++;
                        
                        var mime = 'application/x-font-ttf';
                        
                        // See https://stackoverflow.com/questions/2871655/proper-mime-type-for-fonts
                        if (format == 'svg' || /(\.svg)($|\?)/i.test(url))
                        {
                            mime = 'image/svg+xml';
                        }
                        else if (format == 'otf' || format == 'embedded-opentype' || /(\.otf)($|\?)/i.test(url))
                        {
                            mime = 'application/x-font-opentype';
                        }
                        else if (format == 'woff' || /(\.woff)($|\?)/i.test(url))
                        {
                            mime = 'application/font-woff';
                        }
                        else if (format == 'woff2' || /(\.woff2)($|\?)/i.test(url))
                        {
                            mime = 'application/font-woff2';
                        }
                        else if (format == 'eot' || /(\.eot)($|\?)/i.test(url))
                        {
                            mime = 'application/vnd.ms-fontobject';
                        }
                        else if (format == 'sfnt' || /(\.sfnt)($|\?)/i.test(url))
                        {
                            mime = 'application/font-sfnt';
                        }

						this.mapFontUrl(mime, url, mxUtils.bind(this, function(realMime, realUrl)
						{
							// LATER: Remove cache-control header
							this.loadUrl(realUrl, mxUtils.bind(this, function(uri)
							{
								this.cachedFonts[url] = uri;
								waiting--;
								finish();
							}), mxUtils.bind(this, function(err)
							{
								// LATER: handle error
								waiting--;
								finish();
							}), true, null, 'data:' + realMime + ';charset=utf-8;base64,');
						}));
                    }
                }))(Editor.trimCssUrl(parts[i].substring(0, idx)), format);
            }
            
            //In case all fonts are cached
            finish();
        }
        else
    	{
        	//No font urls found
        	then(fontCss);
    	}
    };
	
	/**
	 * For the fontCSS to be applied when rendering images on canvas, the actual
	 * font data must be made available via a data URI encoding of the file.
	 */
    Editor.prototype.loadFonts = function(then)
    {
        if (this.fontCss != null && this.resolvedFontCss == null)
        {
        	this.embedCssFonts(this.fontCss, mxUtils.bind(this, function(resolvedFontCss)
			{
        		this.resolvedFontCss = resolvedFontCss;

				if (then != null)
				{
        			then();
				}
			}));
        }
        else if (then != null)
        {
            then();
        }
    };

	/**
	 * Returns a CSS mapping for the given CSS URL.
	 */
	Editor.prototype.createGoogleFontCache = function()
	{
		var cache = {};

		for (var key in Graph.fontMapping)
		{
			if (Graph.isCssFontUrl(key))
			{
				cache[key] = Graph.fontMapping[key];
			}
		}

		return cache;
	};
    
    /**
     * Embeds external fonts
     */
    Editor.prototype.embedExtFonts = function(callback)
    {
    	var extFonts = this.graph.getCustomFonts();
		
		if (extFonts.length > 0)
		{
			var content = [];
			var waiting = 0;
			
			if (this.cachedGoogleFonts == null)
			{
				this.cachedGoogleFonts = this.createGoogleFontCache();
			}
			
			var googleCssDone = mxUtils.bind(this, function()
			{
				if (waiting == 0)
	            {
					this.embedCssFonts(content.join(''), callback);
	            }
			});
			
			for (var i = 0; i < extFonts.length; i++)
			{
				(mxUtils.bind(this, function(fontName, fontUrl)
				{
					if (Graph.isCssFontUrl(fontUrl))
					{
						if (this.cachedGoogleFonts[fontUrl] == null)
						{
							waiting++;
							
							this.loadUrl(Graph.rewriteGoogleFontUrl(fontUrl), mxUtils.bind(this, function(css)
		                    {
								this.cachedGoogleFonts[fontUrl] = css;
								content.push(css + '\n');
		                        waiting--;
		                        googleCssDone();
		                    }), mxUtils.bind(this, function(err)
		                    {
		                        waiting--;
								content.push('@import url(' + fontUrl + ');\n');
		                        googleCssDone();
		                    }));
						}
						else
						{
							content.push(this.cachedGoogleFonts[fontUrl] + '\n');
						}
					}
					else
					{
						content.push('@font-face {' +
							'font-family: "' + fontName + '";' + 
							'src: url("' + fontUrl + '")}\n');
					}
				}))(extFonts[i].name, extFonts[i].url);
			}
			
			googleCssDone();
		}
		else
		{
			callback();
		}
    };
	
	/**
	 * Copies MathJax CSS into the SVG output.
	 */
	Editor.prototype.addMathCss = function(svgRoot)
	{
		var defs = svgRoot.getElementsByTagName('defs');
		
		if (defs != null && defs.length > 0)
		{
			var styles = document.getElementsByTagName('style');
			
			for (var i = 0; i < styles.length; i++)
			{
				// Ignores style elements with no MathJax CSS
				var content = mxUtils.getTextContent(styles[i]);

				if (content.indexOf('mxPageSelector') < 0 &&
					content.indexOf('MathJax') > 0)
				{
					defs[0].appendChild(styles[i].cloneNode(true));
				}
			}
		}
	};

	/**
	 * Adds the global fontCss configuration.
	 */
	Editor.prototype.addFontCss = function(svgRoot, fontCss)
	{
		fontCss = (fontCss != null) ? fontCss : this.absoluteCssFonts(this.fontCss);

		// Creates defs element if not available
		if (fontCss != null)
		{
			var defsElt = mxUtils.getSvgDefs(svgRoot);

			// Moves imports to separate style element
			var lines = fontCss.split('\n');
			var imports = [];
			var other = [];

			for (var i = 0; i < lines.length; i++)
			{
				if (lines[i].substring(0, 7) == '@import')
				{
					imports.push(lines[i]);
				}
				else
				{
					other.push(lines[i]);
				}
			}
			
			var svgDoc = svgRoot.ownerDocument;
			var style = (svgDoc.createElementNS != null) ?
				svgDoc.createElementNS(mxConstants.NS_SVG, 'style') :
				svgDoc.createElement('style');
			style.setAttribute('type', 'text/css');

			if (imports.length > 0)
			{
				mxUtils.setTextContent(style, imports.join('\n'));
				defsElt.appendChild(style);
			}

			if (other.length > 0)
			{
				style = style.cloneNode(false);
				mxUtils.setTextContent(style, other.join('\n'));
				defsElt.appendChild(style);
			}
		}
	};
	
	/**
	 * Disables client-side image export if math is enabled.
	 */
	Editor.prototype.isExportToCanvas = function()
	{
		return mxClient.IS_CHROMEAPP || Editor.useCanvasForExport;
	};

	/**
	 * Returns the maximum possible scale for the given canvas dimension and scale.
	 * This will return the given scale or the maximum scale that can be used to
	 * generate a valid image in the current browser.
	 * 
	 * See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas
	 */
	Editor.prototype.getMaxCanvasScale = function(w, h, scale)
	{
		var max = (mxClient.IS_FF) ? 8192 : 16384;

		return Math.min(scale, Math.min(max / w, max / h));
	};
	
	/**
	 *
	 */
	Editor.prototype.exportToCanvas = function(callback, width, imageCache, background, error, limitHeight,
		ignoreSelection, scale, transparentBackground, addShadow, converter, graph, border, noCrop, grid,
		theme, exportType, cells)
	{
		try
		{
			limitHeight = (limitHeight != null) ? limitHeight : true;
			ignoreSelection = (ignoreSelection != null) ? ignoreSelection : true;
			graph = (graph != null) ? graph : this.graph;
			border = (border != null) ? border : 0;
			
			var bg = (transparentBackground) ? null : graph.background;
			
			if (bg == mxConstants.NONE)
			{
				bg = null;
			}
			
			if (bg == null)
			{
				bg = background;
			}
			
			// Handles special case where background is null but transparent is false
			if (bg == null && transparentBackground == false)
			{
				bg = 'light-dark(#ffffff,' + Editor.darkColor + ')';
			}

			this.convertImages(graph.getSvg(null, null, border, noCrop, null, ignoreSelection,
				null, null, null, addShadow, null, theme, exportType, cells),
				mxUtils.bind(this, function(svgRoot)
			{
				try
				{
					svgRoot.style.setProperty('-webkit-font-smoothing', 'antialiased');
					svgRoot.style.setProperty('-moz-osx-font-smoothing', 'grayscale');
					
					var img = new Image();
					
					img.onload = mxUtils.bind(this, function()
					{
				   		try
				   		{
				   			var canvas = document.createElement('canvas');
							var w = parseInt(svgRoot.getAttribute('width'));
							var h = parseInt(svgRoot.getAttribute('height'));
							scale = (scale != null) ? scale : 1;
							
							if (width != null)
							{
								scale = (!limitHeight) ? width / w : Math.min(1, Math.min((width * 3) / (h * 4), width / w));
							}
							
							scale = this.getMaxCanvasScale(w, h, scale);
							w = Math.ceil(scale * w);
							h = Math.ceil(scale * h);
							
							canvas.setAttribute('width', w);
					   		canvas.setAttribute('height', h);
					   		var ctx = canvas.getContext('2d');
					   		
					   		if (bg != null)
					   		{
								var cssBg = mxUtils.getLightDarkColor(bg);
					   			ctx.beginPath();
								ctx.rect(0, 0, w, h);
								ctx.fillStyle = (theme == 'dark' &&
									graph.getAdaptiveColors() != 'none') ?
									cssBg.dark : cssBg.light;
								ctx.fill();
					   		}
		
					   		if (scale != 1)
					   		{
					   			ctx.scale(scale, scale);
					   		}
	
						    function drawImage()
						    {
						    	// Workaround for broken data URI images in Safari on first export
						   		if (mxClient.IS_SF)
						   		{			   		
									window.setTimeout(function()
									{
										ctx.drawImage(img, 0, 0);
										callback(canvas, svgRoot);
									}, 0);
						   		}
						   		else
						   		{
						   			ctx.drawImage(img, 0, 0);
						   			callback(canvas, svgRoot);
						   		}
						    };
						    
						    if (grid)
						    {
							    var view = graph.view;
							    var curViewScale = view.scale;
							    view.scale = 1; //Reset the scale temporary to generate unscaled grid image which is then scaled
								var gridImage = btoa(unescape(encodeURIComponent(view.createSvgGrid(view.gridColor))));
								view.scale = curViewScale;
								gridImage = 'data:image/svg+xml;base64,' + gridImage;
				                var phase = graph.gridSize * view.gridSteps * scale;
				                
				                var b = graph.getGraphBounds();
								var tx = view.translate.x * curViewScale;
								var ty = view.translate.y * curViewScale;
								var x0 = tx + (b.x - tx) / curViewScale - border;
								var y0 = ty + (b.y - ty) / curViewScale - border;
								
								var background = new Image();
		
								background.onload = function()
								{
									try
									{
										var x = -Math.round(phase - mxUtils.mod((tx - x0) * scale, phase));
										var y = -Math.round(phase - mxUtils.mod((ty - y0) * scale, phase));
			
										for (var i = x; i < w; i += phase)
										{
											for (var j = y; j < h; j += phase)
											{
												ctx.drawImage(background, i / scale, j / scale);	
											}
										}
									
										drawImage();
									}
							   		catch (e)
							   		{
							   			if (error != null)
										{
											error(e);
										}
							   		}
								};
								
								background.onerror = function(e)
								{
									if (error != null)
									{
										error(e);
									}
								};
								
								background.src = gridImage;
						    }
						    else
					    	{
						    	drawImage();
					    	}
				   		}
				   		catch (e)
				   		{
				   			if (error != null)
							{
								error(e);
							}
				   		}
					});
					
					img.onerror = function(e)
					{
						//console.log('img', e, img.src);
						
						if (error != null)
						{
							error(e);
						}
					};

					if (addShadow)
					{
						this.graph.addSvgShadow(svgRoot);
					}
					
					if (this.graph.mathEnabled)
					{
						this.addMathCss(svgRoot);
					}
					
					var done = mxUtils.bind(this, function()
					{
						try
						{
							if (this.resolvedFontCss != null)
							{
								this.addFontCss(svgRoot, this.resolvedFontCss);
							}
							
							img.src = Editor.createSvgDataUri(mxUtils.getXml(svgRoot));
						}
						catch (e)
						{
							if (error != null)
							{
								error(e);
							}
						}
					});
					
					this.embedExtFonts(mxUtils.bind(this, function(extFontsEmbeddedCss)
					{
						try
						{
							if (extFontsEmbeddedCss != null)
							{
								this.addFontCss(svgRoot, extFontsEmbeddedCss);
							}
							
							this.loadFonts(done);
						}
						catch (e)
						{
							if (error != null)
							{
								error(e);
							}
						}
					}));
				}
				catch (e)
				{
					//console.log('src', e, img.src);
					
					if (error != null)
					{
						error(e);
					}
				}
			}), imageCache, converter);
		}
		catch (e)
		{
			if (error != null)
			{
				error(e);
			}
		}
	};
	
	Editor.crcTable = [];
	
	for (var n = 0; n < 256; n++)
	{
		var c = n;
		
		for (var k = 0; k < 8; k++)
		{
			if ((c & 1) == 1)
			{
				c = 0xedb88320 ^ (c >>> 1);
			}
			else
			{
				c >>>= 1;
			}

			Editor.crcTable[n] = c;
		}
	}
	
	Editor.updateCRC = function(crc, data, off, len)
	{
		var c = crc;
	
		for (var n = 0; n < len; n++)
		{
			c = Editor.crcTable[(c ^ data.charCodeAt(off + n)) & 0xff] ^ (c >>> 8);
		}
	
		return c;
	};

	Editor.crc32 = function(str)
	{
	    var crc = 0 ^ (-1);

	    for (var i = 0; i < str.length; i++ )
	    {
	        crc = (crc >>> 8) ^ Editor.crcTable[(crc ^ str.charCodeAt(i)) & 0xFF];
	    }

	    return (crc ^ (-1)) >>> 0;
	};

	/**
	 * Adds the given text to the compressed or non-compressed text chunk.
	 */
	Editor.writeGraphModelToPng = function(data, type, key, value, error)
	{
		var base64 = data.substring(data.indexOf(',') + 1);
		var f = (window.atob) ? atob(base64) : Base64.decode(base64, true);
		var pos = 0;
		
		function fread(d, count)
		{
			var start = pos;
			pos += count;
			
			return d.substring(start, pos);
		};
		
		// Reads unsigned long 32 bit big endian
		function _freadint(d)
		{
			var bytes = fread(d, 4);
			
			return bytes.charCodeAt(3) + (bytes.charCodeAt(2) << 8) +
				(bytes.charCodeAt(1) << 16) + (bytes.charCodeAt(0) << 24);
		};
		
		function writeInt(num)
		{
			return String.fromCharCode((num >> 24) & 0x000000ff, (num >> 16) & 0x000000ff,
				(num >> 8) & 0x000000ff, num & 0x000000ff);
		};
		
		// Checks signature
		if (fread(f,8) != String.fromCharCode(137) + 'PNG' + String.fromCharCode(13, 10, 26, 10))
		{
			if (error != null)
			{
				error();
			}
			
			return;
		}
		
		// Reads header chunk
		fread(f,4);
		
		if (fread(f,4) != 'IHDR')
		{
			if (error != null)
			{
				error();
			}
			
			return;
		}
		
		fread(f, 17);
		var result = f.substring(0, pos);
		
		do
		{
			var n = _freadint(f);
			var chunk = fread(f,4);
			
			if (chunk == 'IDAT')
			{
				result = f.substring(0, pos - 8);
				
				if (type == 'pHYs' && key == 'dpi')
				{
					var dpm = Math.round(value / 0.0254); //One inch is equal to exactly 0.0254 meters.
					var chunkData = writeInt(dpm) + writeInt(dpm) + String.fromCharCode(1);
				}
				else
				{
					var chunkData = key + String.fromCharCode(0) +
						((type == 'zTXt') ? String.fromCharCode(0) : '') + 
						value;
				}
				
				var crc = 0xffffffff;
				crc = Editor.updateCRC(crc, type, 0, 4);
				crc = Editor.updateCRC(crc, chunkData, 0, chunkData.length);
				
				result += writeInt(chunkData.length) + type + chunkData + writeInt(crc ^ 0xffffffff);
				result += f.substring(pos - 8, f.length);
				
				break;
			}
			
			result += f.substring(pos - 8, pos - 4 + n);
			fread(f,n);
			fread(f,4);
		}
		while (n);
		
		return 'data:image/png;base64,' + ((window.btoa) ? btoa(result) : Base64.encode(result, true));
	};

	/**
	 * Adds persistence for recent colors
	 */
	if (window.ColorDialog)
	{
		FilenameDialog.filenameHelpLink = 'https://www.drawio.com/doc/faq/save-file-formats'; 
		
		var colorDialogAddRecentColor = ColorDialog.addRecentColor;
		
		ColorDialog.addRecentColor = function(color, max)
		{
			colorDialogAddRecentColor.apply(this, arguments);
			
			mxSettings.setRecentColors(ColorDialog.recentColors);
			mxSettings.save();
		};
		
		var colorDialogResetRecentColors = ColorDialog.resetRecentColors;
		
		ColorDialog.resetRecentColors = function()
		{
			colorDialogResetRecentColors.apply(this, arguments);
			
			mxSettings.setRecentColors(ColorDialog.recentColors);
			mxSettings.save();
		};
	}
	
	// Overrides ID for pages
	if (typeof window.EditDataDialog !== 'undefined')
	{
		EditDataDialog.getDisplayIdForCell = function(ui, cell)
		{
			var id = null;
			
			if (ui.editor.graph.getModel().getParent(cell) != null)
			{
				id = cell.getId();
			}
			else if (ui.currentPage != null)
			{
				id = ui.currentPage.getId();
			}
			
			return id;
		};
	}

	var AddCustomPropertyDialog = function(editorUi, callback)
	{
		var row, td;
		
		var table = document.createElement('table');
		var tbody = document.createElement('tbody');
		table.setAttribute('cellpadding', (mxClient.IS_SF) ? '0' : '2');
		
		row = document.createElement('tr');
		
		td = document.createElement('td');
		td.style.fontSize = '10pt';
		td.style.width = '100px';
		mxUtils.write(td, mxResources.get('name', null, 'Name') + ':');
		
		row.appendChild(td);
		
		var nameInput = document.createElement('input');
		nameInput.style.width = '180px';

		td = document.createElement('td');
		td.appendChild(nameInput);
		row.appendChild(td);
		
		tbody.appendChild(row);
			
		row = document.createElement('tr');
		
		td = document.createElement('td');
		td.style.fontSize = '10pt';
		mxUtils.write(td, mxResources.get('type', null, 'Type') + ':');
		
		row.appendChild(td);
		
		var typeSelect = document.createElement('select');
		typeSelect.style.width = '180px';

		var boolOption = document.createElement('option');
		boolOption.setAttribute('value', 'bool');
		mxUtils.write(boolOption, mxResources.get('bool', null, 'Boolean'));
		typeSelect.appendChild(boolOption);
		
		var clrOption = document.createElement('option');
		clrOption.setAttribute('value', 'color');
		mxUtils.write(clrOption, mxResources.get('color', null, 'Color'));
		typeSelect.appendChild(clrOption);
		
		var enumOption = document.createElement('option');
		enumOption.setAttribute('value', 'enum');
		mxUtils.write(enumOption, mxResources.get('enum', null, 'Enumeration'));
		typeSelect.appendChild(enumOption);

		var floatOption = document.createElement('option');
		floatOption.setAttribute('value', 'float');
		mxUtils.write(floatOption, mxResources.get('float', null, 'Float'));
		typeSelect.appendChild(floatOption);

		var intOption = document.createElement('option');
		intOption.setAttribute('value', 'int');
		mxUtils.write(intOption, mxResources.get('int', null, 'Int'));
		typeSelect.appendChild(intOption);
		
		var strOption = document.createElement('option');
		strOption.setAttribute('value', 'string');
		mxUtils.write(strOption, mxResources.get('string', null, 'String'));
		typeSelect.appendChild(strOption);

		td = document.createElement('td');
		td.appendChild(typeSelect);
		row.appendChild(td);
		
		tbody.appendChild(row);
		
		row = document.createElement('tr');

		td = document.createElement('td');
		td.style.fontSize = '10pt';
		mxUtils.write(td, mxResources.get('dispName', null, 'Display Name') + ':');
		
		row.appendChild(td);
		
		var dispNameInput = document.createElement('input');
		dispNameInput.style.width = '180px';

		td = document.createElement('td');
		td.appendChild(dispNameInput);
		row.appendChild(td);

		tbody.appendChild(row);

		var listRow = document.createElement('tr');

		td = document.createElement('td');
		td.style.fontSize = '10pt';
		mxUtils.write(td, mxResources.get('enumList', null, 'Enum List') + ' (csv):');
		
		listRow.appendChild(td);
		
		var enumListInput = document.createElement('input');
		enumListInput.style.width = '180px';

		td = document.createElement('td');
		td.appendChild(enumListInput);
		listRow.appendChild(td);

		listRow.style.display = 'none';
		tbody.appendChild(listRow);
		
		table.appendChild(tbody);
		
		function typeChanged()
		{
			if (typeSelect.value === 'enum')
			{
				listRow.style.display = '';
				this.container.parentNode.style.height = "150px";
				
			}
			else
			{
				listRow.style.display = 'none';
				this.container.parentNode.style.height = "130px";
			}
		};
		
		mxEvent.addListener(typeSelect, 'change', mxUtils.bind(this, typeChanged));

		row = document.createElement('tr');
		td = document.createElement('td');
		td.setAttribute('align', 'right');
		td.style.paddingTop = '22px';
		td.colSpan = 2;
		
		var addBtn = mxUtils.button(mxResources.get('add', null, 'Add'), mxUtils.bind(this, function()
		{
	    	var name = nameInput.value;

	    	if (name == "")
    		{
	    		nameInput.style.border = "1px solid red";
	    		return;
    		}
	    	
			var type = typeSelect.value;
	    	var dispName = dispNameInput.value;

	    	if (dispName == "")
    		{
	    		dispNameInput.style.border = "1px solid red";
	    		return;
    		}

	    	var enumList = enumListInput.value;
			
	    	if (enumList == "" && type == "enum")
    		{
	    		enumListInput.style.border = "1px solid red";
	    		return;
	    		
    		}
	    	
			if (enumList != null)
			{
				enumList = enumList.split(',');
				
				for (var i = 0; i < enumList.length; i++)
				{
					enumList[i] = enumList[i].trim();
				}
			}
			
			if (callback)
			{
				callback(editorUi, name, type, dispName, enumList);
				editorUi.hideDialog();
			}
		}));
		addBtn.className = 'geBtn gePrimaryBtn';
		
		var cancelBtn = mxUtils.button(mxResources.get('cancel'), function()
		{
			editorUi.hideDialog();
		});
		cancelBtn.className = 'geBtn';
		
		if (editorUi.editor.cancelFirst)
		{
			td.appendChild(cancelBtn);
			td.appendChild(addBtn);
		}
		else
		{
			td.appendChild(addBtn);
			td.appendChild(cancelBtn);
		}

		row.appendChild(td);
		tbody.appendChild(row);
		table.appendChild(tbody);
		this.container = table;
	};
	
	if (window.StyleFormatPanel != null)
	{
		var formatInit = Format.prototype.init;
		
		Format.prototype.init = function()
		{
			formatInit.apply(this, arguments);

			this.editorUi.editor.addListener('fileLoaded', this.update);
		};

		var formatRefresh = Format.prototype.refresh;
		
		Format.prototype.refresh = function()
		{
			if (this.editorUi.getCurrentFile() != null || urlParams['embed'] == '1' ||
				this.editorUi.editor.chromeless)
			{
				formatRefresh.apply(this, arguments);
			}
			else
			{
				this.clear();
			}
		};

		/**
		 * Option is not visible in default theme.
		 */
	    DiagramFormatPanel.prototype.isMathOptionVisible = function(div)
	    {
			return (Editor.currentTheme == 'simple' ||
				Editor.currentTheme == 'sketch' ||
				Editor.currentTheme == 'min');
	    };
	    
		/**
		 * Adds autosave and math typesetting options.
		 */
		var diagramFormatPanelAddOptions = DiagramFormatPanel.prototype.addOptions;

		DiagramFormatPanel.prototype.addOptions = function(div)
		{
			div = diagramFormatPanelAddOptions.apply(this, arguments);
			
			var ui = this.editorUi;
			var editor = ui.editor;
			var graph = editor.graph;

			// Adds autosave option
			if (graph.isEnabled())
			{
				var file = ui.getCurrentFile();

				if (file != null && file.isAutosaveOptional())
				{
					div.appendChild(this.createOption(mxResources.get('autosave'), function()
					{
						return ui.editor.autosave;
					}, function(checked)
					{
						ui.editor.setAutosave(checked);
						
						if (ui.editor.autosave && file.isModified())
						{
							file.fileChanged();
						}
					},
					{
						install: function(apply)
						{
							this.listener = function()
							{
								apply(ui.editor.autosave);
							};
							
							ui.editor.addListener('autosaveChanged', this.listener);
						},
						destroy: function()
						{
							ui.editor.removeListener(this.listener);
						}
					}));
				}
			}
			
			// Adds math option
	        if (this.isMathOptionVisible() && graph.isEnabled() && typeof(MathJax) !== 'undefined')
	        {
				var option = this.createOption(mxResources.get('mathematicalTypesetting'), function()
				{
					return graph.mathEnabled;
				}, function(checked)
				{
					ui.actions.get('mathematicalTypesetting').funct();
				},
				{
					install: function(apply)
					{
						this.listener = function()
						{
							apply(graph.mathEnabled);
						};
						
						ui.addListener('mathEnabledChanged', this.listener);
					},
					destroy: function()
					{
						ui.removeListener(this.listener);
					}
				});
				
				div.appendChild(option);
				
				if (!ui.isOffline() || mxClient.IS_CHROMEAPP || EditorUi.isElectronApp)
				{
					option.appendChild(ui.menus.createHelpLink(
						'https://www.drawio.com/doc/faq/math-typesetting'));
				}
			}
			
			return div;
		};

		mxCellRenderer.prototype.defaultVertexShape.prototype.customProperties = [
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: mxConstants.LINE_ARCSIZE},
	        {name: 'absoluteArcSize', dispName: 'Abs. Arc Size', type: 'bool', defVal: false}
	      ];

		mxCellRenderer.defaultShapes['link'].prototype.customProperties = [
	        {name: 'width', dispName: 'Width', type: 'float', min:0, defVal: 4}
		];

		mxCellRenderer.defaultShapes['flexArrow'].prototype.customProperties = [
	        {name: 'width', dispName: 'Width', type: 'float', min:0, defVal: 10},
	        {name: 'startWidth', dispName: 'Start Width', type: 'float', min:0, defVal: 20},
	        {name: 'endWidth', dispName: 'End Width', type: 'float', min:0, defVal: 20}
		];

		mxCellRenderer.defaultShapes['process'].prototype.customProperties = [
			{name: 'size', dispName: 'Indent', type: 'float', min: 0, max: 0.5, defVal: 0.1}
		];

		mxCellRenderer.defaultShapes['rhombus'].prototype.customProperties = [
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, max: 50, defVal: mxConstants.LINE_ARCSIZE},
	        {name: 'double', dispName: 'Double', type: 'bool', defVal: false}
		];
		
		mxCellRenderer.defaultShapes['partialRectangle'].prototype.customProperties = [
	        {name: 'top', dispName: 'Top Line', type: 'bool', defVal: true},
	        {name: 'bottom', dispName: 'Bottom Line', type: 'bool', defVal: true},
	        {name: 'left', dispName: 'Left Line', type: 'bool', defVal: true},
	        {name: 'right', dispName: 'Right Line', type: 'bool', defVal: true}
        ];
		
		mxCellRenderer.defaultShapes['parallelogram'].prototype.customProperties = [
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: mxConstants.LINE_ARCSIZE},
			{name: 'size', dispName: 'Slope Angle', type: 'float', min:0, max: 1, defVal: 0.2, isVisible: function(state)
			{
				return mxUtils.getValue(state.style, 'fixedSize', '0') == '0';
			}},
	        {name: 'size', dispName: 'Slope Angle', type: 'float', min:0, defVal: 20, isVisible: function(state)
			{
				return mxUtils.getValue(state.style, 'fixedSize', '0') == '1';
			}}
		];
		
		mxCellRenderer.defaultShapes['hexagon'].prototype.customProperties = [
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: mxConstants.LINE_ARCSIZE},
			{name: 'size', dispName: 'Slope Angle', type: 'float', min:0, max: 1, defVal: 0.25, isVisible: function(state)
			{
				return mxUtils.getValue(state.style, 'fixedSize', '0') == '0';
			}},
	        {name: 'size', dispName: 'Slope Angle', type: 'float', min:0, defVal: 25, isVisible: function(state)
			{
				return mxUtils.getValue(state.style, 'fixedSize', '0') == '1';
			}}
		];
		
		mxCellRenderer.defaultShapes['triangle'].prototype.customProperties = [
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: mxConstants.LINE_ARCSIZE}
		];
		
		mxCellRenderer.defaultShapes['document'].prototype.customProperties = [
	        {name: 'size', dispName: 'Size', type: 'float', defVal: 0.3, min:0, max:1}
		];
		
		mxCellRenderer.defaultShapes['internalStorage'].prototype.customProperties = [
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: mxConstants.LINE_ARCSIZE},
	        {name: 'dx', dispName: 'Left Line', type: 'float', min:0, defVal: 20},
	        {name: 'dy', dispName: 'Top Line', type: 'float', min:0, defVal: 20}
		];
		
		mxCellRenderer.defaultShapes['cube'].prototype.customProperties = [
	        {name: 'size', dispName: 'Size', type: 'float', min:0, defVal:20 },
	        {name: 'darkOpacity', dispName: 'Dark Opacity', type: 'float', min:-1, max:1, defVal:0 },
	        {name: 'darkOpacity2', dispName: 'Dark Opacity 2', type: 'float', min:-1, max:1, defVal:0 }
		];
		
		mxCellRenderer.defaultShapes['step'].prototype.customProperties = [
			{name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: mxConstants.LINE_ARCSIZE},
	        {name: 'size', dispName: 'Notch Size', type: 'float', min:0, defVal:20},
	        {name: 'fixedSize', dispName: 'Fixed Size', type: 'bool', defVal:true}
		];
		
		mxCellRenderer.defaultShapes['trapezoid'].prototype.customProperties = [
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: mxConstants.LINE_ARCSIZE},
	        {name: 'size', dispName: 'Slope Angle', type: 'float', min:0, max: 1, defVal: 0.2}
		];
		
		mxCellRenderer.defaultShapes['tape'].prototype.customProperties = [
	        {name: 'size', dispName: 'Size', type: 'float', min:0, max:1, defVal:0.4 }
		];
		
		mxCellRenderer.defaultShapes['note'].prototype.customProperties = [
	        {name: 'size', dispName: 'Fold Size', type: 'float', min:0, defVal: 30},
	        {name: 'darkOpacity', dispName: 'Dark Opacity', type: 'float', min:-1, max:1, defVal:0 },
	    ];
		
		mxCellRenderer.defaultShapes['card'].prototype.customProperties = [
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: mxConstants.LINE_ARCSIZE},
	        {name: 'size', dispName: 'Cutoff Size', type: 'float', min:0, defVal: 30}
	    ];
		
		mxCellRenderer.defaultShapes['callout'].prototype.customProperties = [
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: mxConstants.LINE_ARCSIZE},
	        {name: 'base', dispName: 'Callout Width', type: 'float', min:0, defVal: 20},
	        {name: 'size', dispName: 'Callout Length', type: 'float', min:0, defVal: 30},
	        {name: 'position', dispName: 'Callout Position', type: 'float', min:0, max:1, defVal: 0.5},
	        {name: 'position2', dispName: 'Callout Tip Position', type: 'float', min:0, max:1, defVal: 0.5}
	    ];
		
		mxCellRenderer.defaultShapes['folder'].prototype.customProperties = [
	        {name: 'tabWidth', dispName: 'Tab Width', type: 'float'},
	        {name: 'tabHeight', dispName: 'Tab Height', type: 'float'},
	        {name: 'tabPosition', dispName: 'Tap Position', type: 'enum',
	        	enumList: [{val: 'left', dispName: 'Left'}, {val: 'right', dispName: 'Right'}]
	        }
	    ];
		
		mxCellRenderer.defaultShapes['swimlane'].prototype.customProperties = [
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: 15},
	        {name: 'absoluteArcSize', dispName: 'Abs. Arc Size', type: 'bool', defVal: false},
	        {name: 'startSize', dispName: 'Header Size', type: 'float'},
			{name: 'swimlaneHead', dispName: 'Head Border', type: 'bool', defVal: true},
			{name: 'swimlaneBody', dispName: 'Body Border', type: 'bool', defVal: true},
	        {name: 'horizontal', dispName: 'Horizontal', type: 'bool', defVal: true},
	        {name: 'separatorColor', dispName: 'Separator Color', type: 'color', defVal: null},
	    ];
		
		mxCellRenderer.defaultShapes['table'].prototype.customProperties = [
			{name: 'rowLines', dispName: 'Row Lines', type: 'bool', defVal: true},
			{name: 'columnLines', dispName: 'Column Lines', type: 'bool', defVal: true},
			{name: 'fixedRows', dispName: 'Fixed Rows', type: 'bool', defVal: false},
			{name: 'resizeLast', dispName: 'Resize Last Column', type: 'bool', defVal: false},
			{name: 'resizeLastRow', dispName: 'Resize Last Row', type: 'bool', defVal: false}].
			concat(mxCellRenderer.defaultShapes['swimlane'].prototype.customProperties).
			concat(mxCellRenderer.defaultShapes['partialRectangle'].prototype.customProperties);

		mxCellRenderer.defaultShapes['tableRow'].prototype.customProperties =
			mxCellRenderer.defaultShapes['swimlane'].prototype.customProperties.
			concat(mxCellRenderer.defaultShapes['partialRectangle'].prototype.customProperties);
		
		mxCellRenderer.defaultShapes['doubleEllipse'].prototype.customProperties = [
	        {name: 'margin', dispName: 'Indent', type: 'float', min:0, defVal:4}
	    ];
		
		mxCellRenderer.defaultShapes['ext'].prototype.customProperties = [
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: 15},
			{name: 'double', dispName: 'Double', type: 'bool', defVal: false},
	        {name: 'margin', dispName: 'Indent', type: 'float', min: 0, defVal:0}
	    ];
		
		mxCellRenderer.defaultShapes['curlyBracket'].prototype.customProperties = [
			{name: 'rounded', dispName: 'Rounded', type: 'bool', defVal: true},
	        {name: 'size', dispName: 'Size', type: 'float', min:0, max: 1, defVal: 0.5}
	    ];
		
		mxCellRenderer.defaultShapes['image'].prototype.customProperties = [
			{name: 'imageAspect', dispName: 'Fixed Image Aspect', type: 'bool', defVal:true}
	    ];
		
		mxCellRenderer.defaultShapes['label'].prototype.customProperties = [
			{name: 'imageAspect', dispName: 'Fixed Image Aspect', type: 'bool', defVal:true},
			{name: 'imageAlign', dispName: 'Image Align', type: 'enum',
				enumList: [{val: 'left', dispName: 'Left'}, 
						   {val: 'center', dispName: 'Center'}, 
						   {val: 'right', dispName: 'Right'}], defVal: 'left'},
			{name: 'imageVerticalAlign', dispName: 'Image Vertical Align', type: 'enum',
				enumList: [{val: 'top', dispName: 'Top'}, 
					       {val: 'middle', dispName: 'Middle'}, 
					       {val: 'bottom', dispName: 'Bottom'}], defVal: 'middle'},
	        {name: 'imageWidth', dispName: 'Image Width', type: 'float', min:0, defVal: 24},
	        {name: 'imageHeight', dispName: 'Image Height', type: 'float', min:0, defVal: 24},
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: 12},
	        {name: 'absoluteArcSize', dispName: 'Abs. Arc Size', type: 'bool', defVal: false}
	    ];
		
		mxCellRenderer.defaultShapes['dataStorage'].prototype.customProperties = [
	        {name: 'size', dispName: 'Size', type: 'float', min:0, max:1, defVal:0.1 }
		];
		
		mxCellRenderer.defaultShapes['manualInput'].prototype.customProperties = [
	        {name: 'size', dispName: 'Size', type: 'float', min:0, defVal:30 },
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: 20}
		];
		
		mxCellRenderer.defaultShapes['loopLimit'].prototype.customProperties = [
	        {name: 'size', dispName: 'Size', type: 'float', min:0, defVal:20 },
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: 20}
		];
		
		mxCellRenderer.defaultShapes['offPageConnector'].prototype.customProperties = [
	        {name: 'size', dispName: 'Size', type: 'float', min:0, defVal:38 },
	        {name: 'arcSize', dispName: 'Arc Size', type: 'float', min:0, defVal: 20}
		];
		
		mxCellRenderer.defaultShapes['display'].prototype.customProperties = [
	        {name: 'size', dispName: 'Size', type: 'float', min: 0, max: 1, defVal: 0.25 }
		];
		
		mxCellRenderer.defaultShapes['singleArrow'].prototype.customProperties = [
	        {name: 'arrowWidth', dispName: 'Arrow Width', type: 'float', min: 0, max: 1, defVal: 0.3 },
	        {name: 'arrowSize', dispName: 'Arrowhead Length', type: 'float', min: 0, max: 1, defVal: 0.2 }
		];
		
		mxCellRenderer.defaultShapes['doubleArrow'].prototype.customProperties = [
	        {name: 'arrowWidth', dispName: 'Arrow Width', type: 'float', min: 0, max: 1, defVal: 0.3 },
	        {name: 'arrowSize', dispName: 'Arrowhead Length', type: 'float', min: 0, max: 1, defVal: 0.2 }
		];
		
		mxCellRenderer.defaultShapes['cross'].prototype.customProperties = [
	        {name: 'size', dispName: 'Size', type: 'float', min: 0, max: 1, defVal: 0.2 }
		];
		
		mxCellRenderer.defaultShapes['corner'].prototype.customProperties = [
	        {name: 'dx', dispName: 'Width1', type: 'float', min: 0, defVal: 20 },
	        {name: 'dy', dispName: 'Width2', type: 'float', min: 0, defVal: 20 }
		];
		
		mxCellRenderer.defaultShapes['tee'].prototype.customProperties = [
	        {name: 'dx', dispName: 'Width1', type: 'float', min: 0, defVal: 20 },
	        {name: 'dy', dispName: 'Width2', type: 'float', min: 0, defVal: 20 }
		];
		
		mxCellRenderer.defaultShapes['umlLifeline'].prototype.customProperties = [
			{name: 'participant', dispName:'Participant', type:'enum', defVal:'none', enumList:[
				{val:'none', dispName: 'Default'},	
				{val:'umlActor', dispName: 'Actor'},	
				{val:'umlBoundary', dispName: 'Boundary'},	
				{val:'umlEntity', dispName: 'Entity'},	
				{val:'umlControl', dispName: 'Control'},	
				]},
			{name: 'size', dispName:'Height', type:'float', defVal:40, min:0}
		];
		
		mxCellRenderer.defaultShapes['umlFrame'].prototype.customProperties = [
			{name: 'width', dispName:'Title Width', type:'float', defVal:60, min:0},
			{name: 'height', dispName:'Title Height', type:'float', defVal:30, min:0}
		];
		
		/**
		 * Configures global color schemes.
		 */
		StyleFormatPanel.prototype.defaultColorSchemes = [[{fill: '', stroke: ''}, {fill: '#f5f5f5', stroke: '#666666', font: '#333333'},
			{fill: '#dae8fc', stroke: '#6c8ebf'}, {fill: '#d5e8d4', stroke: '#82b366'},
			{fill: '#ffe6cc', stroke: '#d79b00'}, {fill: '#fff2cc', stroke: '#d6b656'},
			{fill: '#f8cecc', stroke: '#b85450'}, {fill: '#e1d5e7', stroke: '#9673a6'}],
			[{fill: '', stroke: ''}, {fill: '#60a917', stroke: '#2D7600', font: '#ffffff'},
			{fill: '#008a00', stroke: '#005700', font: '#ffffff'}, {fill: '#1ba1e2', stroke: '#006EAF', font: '#ffffff'},
			{fill: '#0050ef', stroke: '#001DBC', font: '#ffffff'}, {fill: '#6a00ff', stroke: '#3700CC', font: '#ffffff'},
			//{fill: '#aa00ff', stroke: '#7700CC', font: '#ffffff'},
			{fill: '#d80073', stroke: '#A50040', font: '#ffffff'}, {fill: '#a20025', stroke: '#6F0000', font: '#ffffff'}],
			[{fill: '#e51400', stroke: '#B20000', font: '#ffffff'}, {fill: '#fa6800', stroke: '#C73500', font: '#000000'},
			{fill: '#f0a30a', stroke: '#BD7000', font: '#000000'}, {fill: '#e3c800', stroke: '#B09500', font: '#000000'},
			{fill: '#6d8764', stroke: '#3A5431', font: '#ffffff'}, {fill: '#647687', stroke: '#314354', font: '#ffffff'},
			{fill: '#76608a', stroke: '#432D57', font: '#ffffff'}, {fill: '#a0522d', stroke: '#6D1F00', font: '#ffffff'}],
			[{fill: '', stroke: ''}, {fill: mxConstants.NONE, stroke: ''},
			{fill: '#fad7ac', stroke: '#b46504'}, {fill: '#fad9d5', stroke: '#ae4132'},
			{fill: '#b0e3e6', stroke: '#0e8088'}, {fill: '#b1ddf0', stroke: '#10739e'},
			{fill: '#d0cee2', stroke: '#56517e'}, {fill: '#bac8d3', stroke: '#23445d'}],
		    [{fill: '', stroke: ''},
			{fill: '#f5f5f5', stroke: '#666666', gradient: '#b3b3b3'},
			{fill: '#dae8fc', stroke: '#6c8ebf', gradient: '#7ea6e0'},
			{fill: '#d5e8d4', stroke: '#82b366', gradient: '#97d077'},
			{fill: '#ffcd28', stroke: '#d79b00', gradient: '#ffa500'},
			{fill: '#fff2cc', stroke: '#d6b656', gradient: '#ffd966'},
			{fill: '#f8cecc', stroke: '#b85450', gradient: '#ea6b66'},
			{fill: '#e6d0de', stroke: '#996185', gradient: '#d5739d'}],
			[{fill: '', stroke: ''}, {fill: '#eeeeee', stroke: '#36393d'},
			{fill: '#f9f7ed', stroke: '#36393d'}, {fill: '#ffcc99', stroke: '#36393d'},
			{fill: '#cce5ff', stroke: '#36393d'}, {fill: '#ffff88', stroke: '#36393d'},
			{fill: '#cdeb8b', stroke: '#36393d'}, {fill: '#ffcccc', stroke: '#36393d'}]];
		
		/**
		 * Configures custom color schemes.
		 */
		StyleFormatPanel.prototype.customColorSchemes = null;
		
		/**
		 * Adds predefiend styles.
		 */
		var styleFormatPanelInit = StyleFormatPanel.prototype.init;
		
		StyleFormatPanel.prototype.init = function()
		{
			var sstate = this.editorUi.getSelectionState();

			if (this.defaultColorSchemes != null && this.defaultColorSchemes.length > 0 &&
				sstate.style.shape != 'image' && !sstate.containsLabel &&
				sstate.cells.length > 0)
			{
				this.container.appendChild(this.addStyles(this.createPanel()));
			}
			
			styleFormatPanelInit.apply(this, arguments);

			if (sstate.customProperties != null)
			{
				this.container.appendChild(this.addProperties(
					this.createPanel(), sstate.customProperties,
					sstate));
			}
		};

		/**
		 * Overridden to add copy and paste style.
		 */
		var styleFormatPanelAddStyleOps = StyleFormatPanel.prototype.addStyleOps;
		
		StyleFormatPanel.prototype.addStyleOps = function(div)
		{
			var ss = this.editorUi.getSelectionState();

			if (ss.cells.length == 1)
			{
				this.addActions(div, ['copyStyle', 'pasteStyle']);
			}
			else if (ss.cells.length >= 1)
			{
				this.addActions(div, ['pasteStyle', 'pasteData']);
			}

			styleFormatPanelAddStyleOps.apply(this, arguments);
			
			return div;
		};

		/**
		 * Initial collapsed state of the properties panel.
		 */
		EditorUi.prototype.propertiesCollapsed = true;

		/**
		 * Create Properties Panel
		 */
		BaseFormatPanel.prototype.addProperties = function(div, properties, state, hideId)
		{
			var that = this;
			var graph = this.editorUi.editor.graph;
			var secondLevel = [];
			
			function insertAfter(newElem, curElem)
			{
				curElem.parentNode.insertBefore(newElem, curElem.nextSibling);
			};
			
			function applyStyleVal(pName, newVal, prop, delIndex, input)
			{
				if (prop.valueChanged != null)
				{
					prop.valueChanged(newVal, input, state, that);
				}
				else
				{
					graph.getModel().beginUpdate();
					try
					{
						var changedProps = [];
						var changedVals = [];

						if (prop.index != null)
						{
							var allVals = [];
							var curVal = prop.parentRow.nextSibling;
							
							while(curVal && curVal.getAttribute('data-pName') == pName)
							{
								allVals.push(curVal.getAttribute('data-pValue'));
								curVal = curVal.nextSibling;
							}
							
							if (prop.index < allVals.length)
							{
								if (delIndex != null)
								{
									allVals.splice(delIndex, 1);
								}
								else
								{
									allVals[prop.index] = newVal;
								}
							}
							else
							{
								allVals.push(newVal);
							}
							
							if (prop.size != null && allVals.length > prop.size) //trim the array to the specifies size
							{
								allVals = allVals.slice(0, prop.size);
							}
							
							newVal = allVals.join(',');
							
							if (prop.countProperty != null)
							{
								graph.setCellStyles(prop.countProperty, allVals.length, graph.getSelectionCells());
								
								changedProps.push(prop.countProperty);
								changedVals.push(allVals.length);
							}
						}

						graph.setCellStyles(pName, newVal, graph.getSelectionCells());
						changedProps.push(pName);
						changedVals.push(newVal);
						
						if (prop.dependentProps != null)
						{
							for (var i = 0; i < prop.dependentProps.length; i++)
							{
								var defVal = prop.dependentPropsDefVal[i];
								var vals = prop.dependentPropsVals[i];
								
								if (vals.length > newVal)
								{
									vals = vals.slice(0, newVal);
								}
								else
								{
									for (var j = vals.length; j < newVal; j++)
									{
										vals.push(defVal);
									}
								}
								
								vals = vals.join(',');
								graph.setCellStyles(prop.dependentProps[i], vals, graph.getSelectionCells());
								changedProps.push(prop.dependentProps[i]);
								changedVals.push(vals);
							}
						}
						
						if (typeof(prop.onChange) == 'function')
						{
							prop.onChange(graph, newVal);
						}
						
						that.editorUi.fireEvent(new mxEventObject('styleChanged', 'keys', changedProps,
							'values', changedVals, 'cells', graph.getSelectionCells()));
					}
					finally
					{
						graph.getModel().endUpdate();
					}
				}
			}
			
			function setElementPos(td, elem)
			{
				elem.style.left = '0px';
				elem.style.top = '0px';
				elem.style.position = 'relative';
				elem.style.right = '0px';
				elem.style.bottom = '0px';
				elem.style.width = '100%';
				elem.style.maxWidth = '100%';
				elem.style.maxHeight = '100%';
				elem.style.textAlign = 'left';
			};
			
			function createColorBtn(pName, pValue, prop)
			{
				var clrDiv = document.createElement('div');
				clrDiv.style.width = '32px';
				clrDiv.style.height = '8px';
				clrDiv.style.margin = '2px';
				clrDiv.style.borderStyle = 'solid';
				clrDiv.style.borderWidth = '1px';

				function updateBackground(color)
				{
					if (color == null || color == mxConstants.NONE)
					{
						clrDiv.style.background = 'url(\'' + Dialog.prototype.noColorImage + '\')';
					}
					else if (color == 'default')
					{
						// Default is not supported for properties, this is a fallback
						clrDiv.style.background = '';
					}
					else
					{
						var cssColor = mxUtils.getLightDarkColor(color);

						if (mxUtils.isLightDarkColor(color) &&
							cssColor.light != cssColor.dark)
						{
							clrDiv.style.background = 'linear-gradient(to right bottom, ' +
								cssColor.cssText + ' 50%, ' + mxUtils.invertLightDarkColor(cssColor).
								cssText + ' 50.3%)';
						}
						else
						{
							clrDiv.style.background = color;
						}
					}
				};
				
				updateBackground(pValue);

				btn = mxUtils.button('', mxUtils.bind(that, function(evt)
				{
					this.editorUi.pickColor(pValue, function(color)
					{
						applyStyleVal(pName, color, prop);
						updateBackground(color);
					});
					
					mxEvent.consume(evt);
				}));
				
				btn.style.height = '18px';
				btn.style.width = '40px';
				btn.style.marginTop = '1px';
				btn.style.marginLeft = '1px';
				btn.className = 'geColorBtn';
				
				btn.appendChild(clrDiv);
				return btn;
			};
			
			function createDynArrList(pName, pValue, subType, defVal, countProperty, myRow, flipBkg)
			{
				if (pValue != null)
				{
					var vals = pValue.split(',');
					secondLevel.push({name: pName, values: vals, type: subType,
						defVal: defVal, countProperty: countProperty,
						parentRow: myRow, isDeletable: true, flipBkg: flipBkg});
				}
				
				btn = mxUtils.button('+', mxUtils.bind(that, function(evt)
				{
					var beforeElem = myRow;
					var index = 0;
					
					while (beforeElem.nextSibling != null)
					{
						var cur = beforeElem.nextSibling;
						var elemPName = cur.getAttribute('data-pName');

						if (elemPName == pName)
						{
							beforeElem = beforeElem.nextSibling;
							index++;
						}
						else
						{
							break;
						}
					}
					
					var newProp = {type: subType, parentRow: myRow, index: index,
						isDeletable: true, defVal: defVal, countProperty: countProperty};
					var arrItem = createPropertyRow(pName, '', newProp, index % 2 == 0, flipBkg);
					applyStyleVal(pName, defVal, newProp);
					insertAfter(arrItem, beforeElem);
					
					mxEvent.consume(evt);
				}));
				
				btn.style.height = '16px';
				btn.style.width = '25px';
				btn.className = 'geColorBtn';
				
				return btn;
			};
			
			function createStaticArrList(pName, pValue, subType, defVal, size, myRow, flipBkg)
			{
				if (size > 0)
				{
					var vals = new Array(size);
					
					var curVals = pValue != null? pValue.split(',') : [];
					
					for (var i = 0; i < size; i++) 
					{
						vals[i] = curVals[i] != null? curVals[i] : (defVal != null? defVal : '');
					}
					
					secondLevel.push({name: pName, values: vals, type: subType, defVal: defVal, parentRow: myRow, flipBkg: flipBkg, size: size});
				}
				
				return document.createElement('div'); //empty cell
			};
			
			function createCheckbox(pName, pValue, prop)
			{
				var input = document.createElement('input');
				input.type = 'checkbox';
				input.checked = pValue == '1';
				
				mxEvent.addListener(input, 'change', function() 
				{
					applyStyleVal(pName, input.checked? '1' : '0', prop, null, input);
				});
				return input;
			};
			
			function createPropertyRow(pName, pValue, prop, isOdd, flipBkg)
			{
				var pDiplayName = prop.dispName;
				var pType = prop.type;
				var row = document.createElement('tr');
				row.className = 'gePropRow' + (flipBkg? 'Dark' : '') + (isOdd? 'Alt' : '') + ' gePropNonHeaderRow';
				row.setAttribute('data-pName', pName);
				row.setAttribute('data-pValue', pValue);
				var rightAlig = false;
				
				if (prop.index != null)
				{
					row.setAttribute('data-index', prop.index);
					pDiplayName = (pDiplayName != null? pDiplayName : '') + '[' + prop.index + ']';
					rightAlig = true;
				}
				
				var td = document.createElement('td');
				td.className = 'gePropRowCell';
				var label = mxResources.get(pDiplayName, null, pDiplayName);
				mxUtils.write(td, label);
				td.setAttribute('title', label);
				
				if (rightAlig)
				{
					td.style.textAlign = 'right';
				}
					
				row.appendChild(td);
				td = document.createElement('td');
				td.className = 'gePropRowCell';
				td.setAttribute('title', (pValue != null) ?
					decodeURIComponent(pValue) : mxResources.get('none'));

				mxEvent.addListener(td, 'click', mxUtils.bind(that, function(e)
				{
					// Stops current property editor and triggers DOM update
					if (!mxUtils.isAncestorNode(row, document.activeElement) &&
						mxUtils.isAncestorNode(row.parentNode, document.activeElement) &&
						document.activeElement != null && document.activeElement.blur != null)
					{
						document.activeElement.blur();
						mxEvent.consume(e);
					}
				}));
				
				if (pType == 'color')
				{
					td.appendChild(createColorBtn(pName, pValue, prop));
				}
				else if (pType == 'bool' || pType == 'boolean')
				{
					td.appendChild(createCheckbox(pName, pValue, prop));
				}
				else if (pType == 'enum')
				{
					var pEnumList = prop.enumList;

					td.innerHTML = '';
					let valueDiv = document.createElement('div');
					valueDiv.className = 'gePropValue';
					td.appendChild(valueDiv);
					
					for (var i = 0; i < pEnumList.length; i++)
					{
						var op = pEnumList[i];
						
						if (op.val == pValue)
						{
							mxUtils.write(valueDiv, mxResources.get(op.dispName, null, op.dispName));
							break;
						}
					}
					
					mxEvent.addListener(td, 'click', mxUtils.bind(that, function(e)
					{
						// Ignores event if currently editing this property
						var source = mxEvent.getSource(e);

						if ((source != td && source != valueDiv) ||
							mxEvent.isConsumed(e))
						{
							return;
						}

						valueDiv.innerHTML = '';
						var select = document.createElement('select');
						var nullValue = 'null';
						var nullOption = null;
						setElementPos(td, select);

						for (var i = 0; i < pEnumList.length; i++)
						{
							var op = pEnumList[i];
							var opElem = document.createElement('option');
							opElem.value = mxUtils.htmlEntities(op.val);
							mxUtils.write(opElem, mxResources.get(op.dispName, null, op.dispName));
							select.appendChild(opElem);

							if (op.val == null)
							{
								opElem.value = nullValue;
								nullOption = opElem;
							}
						}
						
						select.value = (pValue == null && nullOption != null) ? nullValue : pValue;
						
						valueDiv.appendChild(select);

						mxEvent.addListener(select, 'change', function()
						{
							var newVal = mxUtils.htmlEntities(select.value);

							if (select[select.selectedIndex] == nullOption ||
								newVal.value == nullValue)
							{
								newVal = null;
							}

							applyStyleVal(pName, newVal, prop);
							//set value triggers a redraw of the panel which removes the select and updates the row
						});
			
						select.focus();

						//FF calls blur on focus! so set the event after focusing (not with selects but to be safe)
						mxEvent.addListener(select, 'blur', function()
						{
							valueDiv.innerHTML = '';

							for (let i = 0; i < pEnumList.length; i++)
							{
								let op = pEnumList[i];

								if (op.val == select.value)
								{
									mxUtils.write(valueDiv, mxResources.get(op.dispName, null, op.dispName));
									break;
								}
							}
						});
					}));
				}
				else if (pType == 'dynamicArr')
				{
					td.appendChild(createDynArrList(pName, pValue, prop.subType, prop.subDefVal, prop.countProperty, row, flipBkg));
				}
				else if (pType == 'staticArr')
				{
					td.appendChild(createStaticArrList(pName, pValue, prop.subType, prop.subDefVal, prop.size, row, flipBkg));
				}
				else if (pType == 'readOnly')
				{
					var inp = document.createElement('input');
					inp.setAttribute('readonly', '');
					inp.value = pValue;
					inp.style.borderWidth = '0px';

					if (pName == 'id' || pName == 'shape')
					{
						row.firstChild.innerHTML = '';
						row.firstChild.setAttribute('colspan', '2');

						inp.style.flexGrow = '1';
						inp.style.marginLeft = '4px';
						inp.style.textAlign = pName == 'id' ?
							'center' : 'left';
						inp.setAttribute('title', pValue);

						var div = document.createElement('div');
						mxUtils.write(div, mxResources.get(pName) + ':');
						div.style.display = 'inline-flex';
						div.style.alignItems = 'center';
						div.style.width = '100%';

						div.appendChild(inp);
						row.firstChild.appendChild(div);
					}
					else
					{
						inp.style.width = '96px';
						td.appendChild(inp);
					}
				}
				else
				{
					td.innerHTML = '';
					let valueDiv = document.createElement('div');
					valueDiv.className = 'gePropValue';
					td.appendChild(valueDiv);
					valueDiv.innerHTML = mxUtils.htmlEntities(decodeURIComponent(pValue));

					mxEvent.addListener(td, 'click', mxUtils.bind(that, function(e)
					{
						// Ignores event if currently editing this property
						var source = mxEvent.getSource(e);

						if ((source != td && source != valueDiv) ||
							mxEvent.isConsumed(e))
						{
							return;
						}

						valueDiv.innerHTML = '';
						var input = document.createElement('input');
						setElementPos(valueDiv, input);
						input.value = decodeURIComponent(pValue);
						input.className = 'gePropEditor';
						
						if ((pType == 'int' || pType == 'float') && !prop.allowAuto)
						{
							input.type = 'number';
							input.step = pType == 'int'? '1' : 'any';
							
							if (prop.min != null)
							{
								input.min = parseFloat(prop.min); 
							}
							
							if (prop.max != null)
							{
								input.max = parseFloat(prop.max);
							}
						}
						
						valueDiv.appendChild(input);

						function setInputVal()
						{
							var inputVal = input.value;
							inputVal = inputVal.length == 0 && pType != 'string' &&
								pType != 'numbers'? 0 : inputVal;
							
							if (prop.allowAuto)
							{
								if (inputVal.trim != null && inputVal.trim().
									toLowerCase() == 'auto')
								{
									inputVal = 'auto';
									pType = 'string';
								}
								else
								{
									inputVal = parseFloat(inputVal);
									inputVal = isNaN(inputVal)? 0 : inputVal;
								}
							}
							
							if (prop.min != null && inputVal < prop.min)
							{
								inputVal = prop.min;
							} 
							else if (prop.max != null && inputVal > prop.max)
							{
								inputVal = prop.max;
							}

							var newVal = null;

							try
							{
								newVal = (pType == 'numbers') ? inputVal.match(/\d+/g).map(Number).join(' ') :
									encodeURIComponent((pType == 'int'? parseInt(inputVal) : inputVal) + '');
							}
							catch(e)
							{
								// ignores parsing errors
							}
							
							applyStyleVal(pName, newVal, prop, null, input);
						}
						
						mxEvent.addListener(input, 'keypress', function(e)
						{
							if (e.keyCode == 13) 
							{
								setInputVal();
								//set value triggers a redraw of the panel which removes the input
							}
						});
						
						input.focus();
						
						//FF calls blur on focus! so set the event after focusing
						mxEvent.addListener(input, 'blur', function() 
						{
							setInputVal();
						});
					}));
				}

				if (prop.isDeletable)
				{
					var delBtn = mxUtils.button('-', mxUtils.bind(that, function(evt)
					{
						//delete the node by refreshing the properties
						applyStyleVal(pName, '', prop, prop.index);
						
						mxEvent.consume(evt);
					}));
					
					delBtn.style.height = '16px';
					delBtn.style.width = '25px';
					delBtn.style.float = 'right';
					delBtn.className = 'geColorBtn';
					td.appendChild(delBtn);
				}
				
				row.appendChild(td);
				return row;
			};
			
			var grid = document.createElement('table');
			grid.className = 'geProperties geFullWidthElement';
			//create header row
			var hrow = document.createElement('tr');
			hrow.className = 'gePropHeader';
			var th = document.createElement('th');
			th.className = 'gePropHeaderCell';
			th.style.paddingLeft = '16px';
			th.style.backgroundRepeat = 'no-repeat';
			th.style.backgroundPosition = '-2px 50%';
			th.style.backgroundSize = '20px';
			mxUtils.write(th, mxResources.get('property'));
			hrow.style.cursor = 'pointer';
			
			var onFold = function()
			{
				var rows = grid.querySelectorAll('.gePropNonHeaderRow');
				var display;
				
				if (!that.editorUi.propertiesCollapsed)
				{
					th.style.backgroundImage = 'url(\'' + Editor.arrowDownImage + '\')';
					display = '';
				}
				else
				{
					th.style.backgroundImage = 'url(\'' + Editor.arrowRightImage + '\')';
					display = 'none';
					
					for (var e = div.childNodes.length - 1; e >= 0 ; e--)
					{
						//Blur can be executed concurrently with this method and the element is removed before removing it here
						try
						{
							var child = div.childNodes[e]; 
							var nodeName = child.nodeName.toUpperCase();
							
							if (nodeName == 'INPUT' || nodeName == 'SELECT')
							{
								div.removeChild(child);
							}
						}
						catch(ex){}
					}
				}
				
				for (var r = 0; r < rows.length; r++)
				{
					rows[r].style.display = display;
				}
			};

			mxEvent.addListener(hrow, 'click', function()
			{
				that.editorUi.propertiesCollapsed = !that.editorUi.propertiesCollapsed;
				onFold();
			});

			hrow.appendChild(th);
			var th2 = document.createElement('th');
			th2.className = 'gePropHeaderCell';
			th2.style.paddingLeft = '4px';
			mxUtils.write(th2, mxResources.get('value'));
			hrow.appendChild(th2);
			grid.appendChild(hrow);
			
			var isOdd = false;
			var flipBkg = false;
			
			var cellId = null;
			
			if (state.vertices.length == 1 && state.edges.length == 0)
			{
				cellId = state.vertices[0].id;
			}
			else if (state.vertices.length == 0 && state.edges.length == 1)
			{
				cellId = state.edges[0].id;
			}
			
			//Add it to top (always)
			if (cellId != null && !hideId)
			{
				grid.appendChild(createPropertyRow('id', mxUtils.htmlEntities(cellId),
					{dispName: 'id', type: 'readOnly'}, true, false));
				
				if (state.style != null && state.style.shape != null)
				{
					grid.appendChild(createPropertyRow('shape', mxUtils.htmlEntities(state.style.shape),
						{dispName: 'shape', type: 'readOnly'}, true, false));
				}
			}
			
			for (var key in properties)
			{
				var prop = properties[key];
				
				if (typeof(prop.isVisible) == 'function')
				{
					if (!prop.isVisible(state, this)) continue;
				}
				
				if (!prop.primary)
				{
					var pValue = (prop.getValue != null) ? prop.getValue(state, this) : (state.style[key] != null? mxUtils.htmlEntities(state.style[key] + '') :
						((prop.getDefaultValue != null) ? prop.getDefaultValue(state, this) : prop.defVal)); //or undefined if defVal is undefined

					if (prop.type == 'separator')
					{
						flipBkg = !flipBkg;
						continue;
					}
					else if (prop.type == 'staticArr') //if dynamic values are needed, a more elegant technique is needed to replace such values
					{
						prop.size = parseInt(state.style[prop.sizeProperty] || properties[prop.sizeProperty].defVal) || 0;
					}
					else if (prop.dependentProps != null)
					{
						var dependentProps = prop.dependentProps;
						var dependentPropsVals = [];
						var dependentPropsDefVal = [];
						
						for (var i = 0; i < dependentProps.length; i++)
						{
							if (properties[dependentProps[i]] != null)
							{
								var propVal = state.style[dependentProps[i]];
								dependentPropsDefVal.push(properties[dependentProps[i]].subDefVal);
								dependentPropsVals.push(propVal != null? propVal.split(',') : []);
							}
						}
						
						prop.dependentPropsDefVal = dependentPropsDefVal;
						prop.dependentPropsVals = dependentPropsVals;
					}
					
					grid.appendChild(createPropertyRow(key, pValue, prop, isOdd, flipBkg));
					
					isOdd = !isOdd;
				}
			}
			
			for (var i = 0; i < secondLevel.length; i++)
			{
				var prop = secondLevel[i];
				var insertElem = prop.parentRow;
					
				for (var j = 0; j < prop.values.length; j++)
				{
					//mxUtils.clone failed because of the HTM element, so manual cloning is used
					var iProp = {type: prop.type, parentRow: prop.parentRow, isDeletable: prop.isDeletable, index: j,
							defVal: prop.defVal, countProperty: prop.countProperty, size: prop.size};
					var arrItem = createPropertyRow(prop.name, prop.values[j], iProp, j % 2 == 0, prop.flipBkg);
					insertAfter(arrItem, insertElem);
					insertElem = arrItem;
				}
			}
			
			div.appendChild(grid);
			onFold();
			
			return div;
		};
		
		/**
		 * Creates the buttons for the predefined styles.
		 */
		StyleFormatPanel.prototype.addStyles = function(div)
		{
			if (this.defaultColorSchemes != null)
			{
				var ui = this.editorUi;
				var graph = ui.editor.graph;
				var picker = document.createElement('div');
				picker.style.padding = '6px 20px 0 24px';
				div.appendChild(picker);

				// Maximum palettes to switch the switcher
				var maxEntries = 10;
							
				// Selector
				var switcher = document.createElement('div');
				switcher.className = 'geSwitcher';
				switcher.style.padding = '4px 18px 6px 0px';
				
				var dots = [];
				
				for (var i = 0; i < this.defaultColorSchemes.length; i++)
				{
					var dot = document.createElement('div');
					dot.className = 'geSwitcherDot';
					
					(mxUtils.bind(this, function(index)
					{
						mxEvent.addListener(dot, 'click', mxUtils.bind(this, function()
						{
							setScheme(index);
						}));
					}))(i);
					
					dots.push(dot);
					switcher.appendChild(dot);
				}
				
				var setScheme = mxUtils.bind(this, function(index)
				{
					if (dots[index] != null)
					{
						if (this.format.currentScheme != null && dots[this.format.currentScheme] != null)
						{
							dots[this.format.currentScheme].style.background = 'transparent';
						}
						
						this.format.currentScheme = index;
						updateScheme(this.defaultColorSchemes[this.format.currentScheme]);
						dots[this.format.currentScheme].style.background = '#84d7ff';
					}
				});
				
				var updateScheme = mxUtils.bind(this, function(colorsets)
				{
					var defaults = mxUtils.clone(graph.defaultVertexStyle);
					defaults[mxConstants.STYLE_STROKECOLOR] = (defaults[mxConstants.STYLE_STROKECOLOR] != null) ?
						defaults[mxConstants.STYLE_STROKECOLOR] : 'default';
					defaults[mxConstants.STYLE_FILLCOLOR] = (defaults[mxConstants.STYLE_FILLCOLOR] != null) ?
						defaults[mxConstants.STYLE_FILLCOLOR] : 'default';
					defaults[mxConstants.STYLE_FONTCOLOR] = (defaults[mxConstants.STYLE_FONTCOLOR] != null) ?
						defaults[mxConstants.STYLE_FONTCOLOR] : 'default';
					graph.replaceDefaultColors(new mxCell(), defaults);

					var addButton = mxUtils.bind(this, function(colorset)
					{
						var btn = mxUtils.button('', mxUtils.bind(this, function(evt)
						{
							graph.getModel().beginUpdate();
							try
							{
								var cells = ui.getSelectionState().cells;
								
								for (var i = 0; i < cells.length; i++)
								{
									var style = graph.getModel().getStyle(cells[i]);
									
									if (colorset != null)
									{
										if (!mxEvent.isShiftDown(evt))
										{
											if (colorset['fill'] == '' || colorset['fill'] == null)
											{
												style = mxUtils.setStyle(style, mxConstants.STYLE_FILLCOLOR, null);
											}
											else
											{
												style = mxUtils.setStyle(style, mxConstants.STYLE_FILLCOLOR, colorset['fill']);
											}

											if (colorset['gradient'] == '' || colorset['gradient'] == null)
											{
												style = mxUtils.setStyle(style, mxConstants.STYLE_GRADIENTCOLOR, null);
											}
											else
											{
												style = mxUtils.setStyle(style, mxConstants.STYLE_GRADIENTCOLOR, colorset['gradient']);
											}

											if (!mxEvent.isControlDown(evt) && (!mxClient.IS_MAC || !mxEvent.isMetaDown(evt)) &&
												graph.getModel().isVertex(cells[i]))
											{
												if (colorset['font'] == '' || colorset['font'] == null)
												{
													style = mxUtils.setStyle(style, mxConstants.STYLE_FONTCOLOR, null);
												}
												else
												{
													style = mxUtils.setStyle(style, mxConstants.STYLE_FONTCOLOR, colorset['font']);
												}
											}
										}
										
										if (!mxEvent.isAltDown(evt))
										{
											if (colorset['stroke'] == '' || colorset['fill'] == null)
											{
												style = mxUtils.setStyle(style, mxConstants.STYLE_STROKECOLOR, null);
											}
											else
											{
												style = mxUtils.setStyle(style, mxConstants.STYLE_STROKECOLOR, colorset['stroke']);
											}
										}
									}
									else
									{
										style = mxUtils.setStyle(style, mxConstants.STYLE_FILLCOLOR, null);
										style = mxUtils.setStyle(style, mxConstants.STYLE_STROKECOLOR, null);
										style = mxUtils.setStyle(style, mxConstants.STYLE_GRADIENTCOLOR, null);
										
										if (graph.getModel().isVertex(cells[i]))
										{
											style = mxUtils.setStyle(style, mxConstants.STYLE_FONTCOLOR, null);
										}
									}

									graph.getModel().setStyle(cells[i], style);
								}
							}
							finally
							{
								graph.getModel().endUpdate();
							}
						}));
		
						btn.className = 'geStyleButton';
						btn.style.width = '36px';
						btn.style.height = (this.defaultColorSchemes.length <= maxEntries) ? '24px' : '30px';
						btn.style.margin = '0px 6px 6px 0px';
						
						if (colorset != null)
						{
							var b = '1px solid';
							
							if (colorset['border'] != null)
							{
								b = colorset['border'];
							}
							
							if (colorset['gradient'] != null)
							{
								btn.style.backgroundImage = 'linear-gradient(' +
									mxUtils.getLightDarkColor(colorset['fill']).cssText + ' 0px,' +
									mxUtils.getLightDarkColor(colorset['gradient']).cssText + ' 100%)';
							}
							else if (colorset['fill'] == mxConstants.NONE)
							{
								btn.style.background = 'url(\'' + Dialog.prototype.noColorImage + '\')';
							}
							else if (colorset['fill'] == null || colorset['fill'] == '')
							{
								btn.style.backgroundColor = mxUtils.getLightDarkColor(
									mxUtils.getValue(defaults, mxConstants.STYLE_FILLCOLOR,
										'#ffffff')).cssText;
							}
							else
							{
								var cssColor = mxUtils.getLightDarkColor(colorset['fill']);
								btn.style.backgroundImage = 'linear-gradient(to right bottom, ' +
									cssColor.cssText + ' 50%, ' + cssColor.light + ' 50.3%)';
							}
							
							if (colorset['stroke'] == null || colorset['stroke'] == mxConstants.NONE)
							{
								btn.style.border = b + ' transparent';
							}
							else if (colorset['stroke'] == '')
							{
								btn.style.border = '1px solid ' + mxUtils.getLightDarkColor(
									mxUtils.getValue(defaults, mxConstants.STYLE_STROKECOLOR,
										'#000000')).cssText;
							}
							else
							{
								var cssColor = mxUtils.getLightDarkColor(colorset['stroke']);
								btn.style.border = b + ' ' + cssColor.cssText;
								btn.style.borderRightColor = cssColor.light;
								btn.style.borderBottomColor = btn.style.borderRightColor;
							}

							if (colorset['title'] != null)
							{
								btn.setAttribute('title', colorset['title']);
							}
						}
						else
						{
							var bg = mxUtils.getValue(defaults, mxConstants.STYLE_FILLCOLOR, '#ffffff');
							var bd = mxUtils.getValue(defaults, mxConstants.STYLE_STROKECOLOR, '#000000');
							
							btn.style.backgroundColor = bg;
							btn.style.border = '1px solid ' + bd;
						}

						btn.style.borderRadius = '0';
						picker.appendChild(btn);

						if (colorset != null && colorset['gradient'] != null)
						{
							var lightBtn = btn.cloneNode(false);
							lightBtn.style.backgroundImage = 'linear-gradient(light-dark(transparent, ' +
								mxUtils.getLightDarkColor(colorset['fill']).light + ') 0px, ' +
									'light-dark(transparent, ' + mxUtils.getLightDarkColor(
										colorset['gradient']).light + ') 100%)';
							lightBtn.style.clipPath = 'polygon(0 100%, 100% 0, 100% 100%)';
							lightBtn.style.backgroundColor = 'transparent';
							picker.appendChild(lightBtn);
							lightBtn.style.marginLeft = '-42px';

							mxEvent.addListener(lightBtn, 'click', function()
							{
								btn.click();
							});
						}
					});
					
					picker.innerText = '';
					
					if (colorsets != null)
					{
						for (var i = 0; i < colorsets.length; i++)
						{
							if (i > 0 && mxUtils.mod(i, 4) == 0)
							{
								mxUtils.br(picker);
							}
							
							addButton(colorsets[i]);
						}
					}
				});

				if (this.format.currentScheme == null)
				{
					setScheme(Math.min(dots.length - 1, Editor.isDarkMode()
						? 1 : (urlParams['sketch'] == '1' ? 5 : 0)));
				}
				else
				{
					setScheme(this.format.currentScheme);
				}
				
				var bottom = (this.defaultColorSchemes.length <= maxEntries) ? 43 : 23;

				var left = document.createElement('div');
				left.className = 'geButton';
				left.style.cssText = 'position:absolute;left:0px;bottom:' + bottom + 'px;width:20px;' +
					'background-image:url(' + Editor.chevronLeftImage + ');';
				
				mxEvent.addListener(left, 'click', mxUtils.bind(this, function()
				{
					setScheme(mxUtils.mod(this.format.currentScheme - 1, this.defaultColorSchemes.length));
				}));
				
				var right = document.createElement('div');
				right.className = 'geButton';
				right.style.cssText = 'position:absolute;left:186px;bottom:' + bottom + 'px;width:20px;' +
					'background-image:url(' + Editor.chevronRightImage + ');';

				if (this.defaultColorSchemes.length > 1)
				{
					div.appendChild(left);
					div.appendChild(right);
				}
				
				mxEvent.addListener(right, 'click', mxUtils.bind(this, function()
				{
					setScheme(mxUtils.mod(this.format.currentScheme + 1, this.defaultColorSchemes.length));
				}));
				
				updateScheme(this.defaultColorSchemes[this.format.currentScheme]);
				
				if (this.defaultColorSchemes.length <= maxEntries)
				{
					div.appendChild(switcher);
				}
			}
			
			return div;
		};
	}
	
	/**
	 * Maps fonts to font-face CSS.
	 */
	Graph.fontMapping = {'https://fonts.googleapis.com/css?family=Architects+Daughter':
		'@font-face { font-family: "Architects Daughter"; ' + 
		'src: url(' + STYLE_PATH + '/fonts/ArchitectsDaughter-Regular.ttf) format("truetype"); }'};

	/**
	 * Lookup table for mapping from font URL and name to elements in the DOM.
	 */
	Graph.customFontElements = {};

	/**
	 * Returns true if the given font URL references a Google font.
	 */
	Graph.isGoogleFontUrl = function(url)
	{
		return url.substring(0, Editor.GOOGLE_FONTS.length) == Editor.GOOGLE_FONTS ||
			url.substring(0, Editor.GOOGLE_FONTS_CSS2.length) == Editor.GOOGLE_FONTS_CSS2;
	};

	/**
	 * Returns true if the given font URL is a CSS file.
	 */
	Graph.isCssFontUrl = function(url)
	{
		return Graph.isGoogleFontUrl(url);
	};

	/**
	 * Uses CSS2 for Google fonts to support bold font style eg.
	 * https://fonts.googleapis.com/css?family=IBM+Plex+Sans is rewritten as
	 * https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500
	 */
	Graph.rewriteGoogleFontUrl = function(url)
	{
		if (url != null && url.substring(0, Editor.GOOGLE_FONTS.length) == Editor.GOOGLE_FONTS)
		{
			url = Editor.GOOGLE_FONTS_CSS2 + url.substring(Editor.GOOGLE_FONTS.length) + ':wght@400;500';
		}

		return url;
	};

	/**
	 * Creates the DOM node for the custom font.
	 */
	Graph.createFontElement = function(name, url)
	{
		var elt = null;
		var style = Graph.fontMapping[url];

		if (style == null && Graph.isCssFontUrl(url))
		{
			elt = document.createElement('link');
			elt.setAttribute('rel', 'stylesheet');
			elt.setAttribute('type', 'text/css');
			elt.setAttribute('charset', 'UTF-8');
			elt.setAttribute('href', Graph.rewriteGoogleFontUrl(url));
		}
		else
		{
			if (style == null)
			{
				style = '@font-face {\n' +	
					'font-family: "' + name + '";\n' + 	
					'src: url("' + url + '");\n}'
			}

			elt = document.createElement('style');
			mxUtils.write(elt, style);
		}
		
		return elt;
	};
		
	/**
	 * Adds an entry to the recent custom fonts list.
	 */
	Graph.addRecentCustomFont = function(key, entry)
	{
		// Hook for registering recent custom fonts in the UI
	};

	/**
	 * Adds a font to the document.
	 */
	Graph.addFont = function(name, url, callback, elementLookup)
	{
		if (name != null && name.length > 0 && url != null && url.length > 0)
		{
			elementLookup = (elementLookup != null) ?
				elementLookup : Graph.customFontElements;
			var key = name.toLowerCase();
			
			// Blocks UI fonts from being overwritten
			if (key != 'helvetica' && name != 'arial' && key != 'sans-serif')
			{
				var entry = elementLookup[key];
				
				// Replaces element if URL has changed
				if (entry != null && entry.url != url)
				{
					entry.elt.parentNode.removeChild(entry.elt);
					entry = null;
				}
				
				if (entry == null)
				{
					var realUrl = url;
					
					// Fixes possible mixed content by using proxy
					if (url.substring(0, 5) == 'http:')
					{
						realUrl = PROXY_URL + '?url=' + encodeURIComponent(url);
					}

					entry = {name: name, url: url, elt: Graph.createFontElement(name, realUrl)};
					elementLookup[key] = entry;
					var head = (elementLookup == Graph.customFontElements) ?
						document.getElementsByTagName('head')[0] : null;
					
					if (callback != null)
					{
						if (entry.elt.nodeName.toLowerCase() == 'link')
						{
							entry.elt.onload = callback;
							entry.elt.onerror = callback;
						}
						else
						{
							callback();
						}
					}
						
					if (head != null)
					{
						head.appendChild(entry.elt);
					}
				}
				else if (callback != null)
				{
					callback();
				}
			}
			else if (callback != null)
			{
				callback();
			}
		}
		else if (callback != null)
		{
			callback();
		}
				
		return name;
	};

	/**
	 * Returns the URL for the given font name if it exists in the document.
	 * Otherwise it returns the given URL.
	 */
	Graph.getFontUrl = function(name, url)
	{
		var font = Graph.customFontElements[name.toLowerCase()];
		
		if (font != null)
		{
			url = font.url;
		}
		
		return url;
	};
	
	/**
	 * Processes the fonts in the given element and its descendants.
	 */
	Graph.processFontAttributes = function(elt)
	{
		var elts = elt.getElementsByTagName('*');
		
		for (var i = 0; i < elts.length; i++)
		{
			var url = elts[i].getAttribute('data-font-src');
			
			if (url != null)
			{
				var name = (elts[i].nodeName == 'FONT') ?
					elts[i].getAttribute('face') :
					elts[i].style.fontFamily;
	
				if (name != null)
				{
					Graph.addFont(name, url);
				}
			}
		}		
	};
	
	/**
	 * Processes the font in the given cell style.
	 */
	Graph.processFontStyle = function(style)
	{
		if (style != null)
		{
			var url = mxUtils.getValue(style, 'fontSource', null);
	
			if (url != null)
			{
				var name = mxUtils.getValue(style, mxConstants.STYLE_FONTFAMILY, null);
				
				if (name != null)
				{
					try
					{
						Graph.addFont(name, decodeURIComponent(url));
					}
					catch (e)
					{
						// ignore
					}
				}
			}
		}
		
		return style;
	};

	/**
	 * Changes the default stylename so that it matches the old named style
	 * if one was specified in the XML.
	 */
	Graph.prototype.defaultThemeName = 'default-style2';
	
	/**
	 * Contains the last XML that was pasted.
	 */
	Graph.prototype.lastPasteXml = null;
	
	/**
	 * Contains the number of times the last XML was pasted.
	 */
	Graph.prototype.pasteCounter = 0;
	
	/**
	 * Graph Overrides
	 */
	Graph.prototype.defaultScrollbars = urlParams['sb'] != '0';

	/**
	 * Specifies if the page should be visible for new files. Default is true.
	 */
	Graph.prototype.defaultPageVisible = urlParams['pv'] != '0';

	/**
	 * Properties for the SVG shadow effect.
	 */
	Graph.prototype.svgShadowColor = '#3D4574';
	
	/**
	 * Properties for the SVG shadow effect.
	 */
	Graph.prototype.svgShadowOpacity = '0.4';

	/**
	 * Properties for the SVG shadow effect.
	 */
	Graph.prototype.svgShadowBlur = '1.7';
	
	/**
	 * Properties for the SVG shadow effect.
	 */
	Graph.prototype.svgShadowSize = '3';

	/**
	 * Enables move of bends/segments without selecting.
	 */
	Graph.prototype.hiddenTags = [];
	
	/**
	 * Enables move of bends/segments without selecting.
	 */
	Graph.prototype.defaultMathEnabled = false;
	
	/**
	 * Adds rack child layout style.
	 */
	var graphInit = Graph.prototype.init;
	
	Graph.prototype.init = function()
	{
		graphInit.apply(this, arguments);

		// Array of hidden tags used in isCellVisible override
		this.hiddenTags = [];

		//TODO initialize Freehand in the correct location!
		if (window.mxFreehand)
		{
			this.freehand = new mxFreehand(this);
		}
		
		// Override insert location for current mouse point
		var mouseEvent = null;
		
		function setMouseEvent(evt)
		{
			mouseEvent = evt;
		};
		
		mxEvent.addListener(this.container, 'mouseenter', setMouseEvent);
		mxEvent.addListener(this.container, 'mousemove', setMouseEvent);
		mxEvent.addListener(this.container, 'mouseleave', function(evt)
		{
			mouseEvent = null;
		});
				
		// Extends getInsertPoint to use the current mouse location
		this.isMouseInsertPoint = function()
		{
			return mouseEvent != null;
		};
		
		var getInsertPoint = this.getInsertPoint;
		
		this.getInsertPoint = function()
		{
			if (mouseEvent != null)
			{
				return this.getPointForEvent(mouseEvent);
			}
			
			return getInsertPoint.apply(this, arguments);
		};
		
		var layoutManagerGetLayout = this.layoutManager.getLayout;
		
		this.layoutManager.getLayout = function(cell)
		{
			// Workaround for possible invalid style after change and before view validation
			var style = this.graph.getCellStyle(cell);
			
			// mxRackContainer may be undefined as it is dynamically loaded at render time
			if (style != null)
			{
				if (style['childLayout'] == 'rack')
				{
					var rackLayout = new mxStackLayout(this.graph, false);
					var unitSize = 20;
					
					if (style['rackUnitSize'] != null)
					{
						rackLayout.gridSize = parseFloat(style['rackUnitSize']);
					}
					else
					{
						rackLayout.gridSize = (typeof mxRackContainer !== 'undefined') ? mxRackContainer.unitSize : unitSize;
					}
					
					rackLayout.marginLeft = style['marginLeft'] || 0;
					rackLayout.marginRight = style['marginRight'] || 0;
					rackLayout.marginTop = style['marginTop'] || 0;
					rackLayout.marginBottom = style['marginBottom'] || 0;
					rackLayout.allowGaps = style['allowGaps'] || 0;
					rackLayout.horizontal = mxUtils.getValue(style, 'horizontalRack', '0') == '1';
					rackLayout.resizeParent = false;
					rackLayout.fill = true;
					
					return rackLayout;
				}
			}
			
			return layoutManagerGetLayout.apply(this, arguments);
		}
		
		this.updateGlobalUrlVariables();
	};

	/**
	 * Adds support for custom fonts in cell styles.
	 */
	var graphPostProcessCellStyle = Graph.prototype.postProcessCellStyle;
	Graph.prototype.postProcessCellStyle = function(cell, style)
	{
		return Graph.processFontStyle(graphPostProcessCellStyle.apply(this, arguments));
	};

	/**
	 * Handles custom fonts in labels.
	 */
	var mxSvgCanvas2DUpdateTextNodes = mxSvgCanvas2D.prototype.updateTextNodes;
	mxSvgCanvas2D.prototype.updateTextNodes = function(x, y, w, h, align, valign, wrap, overflow, clip, rotation, dir, g)
	{
		mxSvgCanvas2DUpdateTextNodes.apply(this, arguments);
		Graph.processFontAttributes(g);
	};
	
	/**
	 * Handles custom fonts in labels.
	 */
	var mxTextRedraw = mxText.prototype.redraw;
	mxText.prototype.redraw = function()
	{
		mxTextRedraw.apply(this, arguments);
		
		// Handles label rendered without foreign object
		if (this.node != null && this.node.nodeName == 'DIV')
		{
			Graph.processFontAttributes(this.node);
		}
	};

	/**
	 * Creates the tags dialog.
	 */
	Graph.prototype.createTagsDialog = function(isEnabled, invert, addFn, helpButton)
	{
		var graph = this;
		var allTags = graph.hiddenTags.slice();
		
		var div = document.createElement('div');
		div.style.userSelect = 'none';
		div.style.overflow = 'hidden';
		div.style.padding = '10px';
		div.style.height = '100%';
		
		var tagCloud = document.createElement('div');
		tagCloud.style.position = 'absolute';
		tagCloud.style.boxSizing = 'border-box';
		tagCloud.style.userSelect = 'none';
		tagCloud.style.overflow = 'auto';
		tagCloud.style.padding = '3px';
		tagCloud.style.left = '0px';
		tagCloud.style.right = '0px';
		tagCloud.style.top = '0px';
		tagCloud.style.bottom = '0px';
		div.appendChild(tagCloud);

		function removeInvisibleSelectionCells()
		{
			var cells = graph.getSelectionCells();
			var visible = [];

			for (var i = 0; i < cells.length; i++)
			{
				if (graph.isCellVisible(cells[i]))
				{
					visible.push(cells[i]);	
				}
			}

			graph.setSelectionCells(visible);
		};

		function setAllVisible(visible)
		{
			graph.setHiddenTags(visible ? [] : allTags.slice());
			removeInvisibleSelectionCells();
			graph.refresh();
		};
		
		graph.addListener(mxEvent.ROOT, function()
		{
			allTags = graph.hiddenTags.slice();
		});
	
		function refreshTags(tags, selected)
		{
			tagCloud.innerText = '';
	
			if (tags.length > 0)
			{
				var table = document.createElement('table');
				table.setAttribute('cellpadding', '2');
				table.style.boxSizing = 'border-box';
				table.style.tableLayout = 'fixed';
				table.style.width = '100%';
	
				var tbody = document.createElement('tbody');

				if (tags != null && tags.length > 0)
				{
					for (var i = 0; i < tags.length; i++)
					{
						(function(tag)
						{
							function setTagVisible()
							{
								var temp = allTags.slice();
								var index = mxUtils.indexOf(temp, tag);
								temp.splice(index, 1);
								graph.setHiddenTags(temp);
								removeInvisibleSelectionCells();
								graph.refresh();
							};

							function selectCells()
							{
								var cells = graph.getCellsForTags(
									[tag], null, null, true);

								if (graph.isEnabled())
								{
									graph.setSelectionCells(cells);
								}
								else
								{
									graph.highlightCells(cells, null, null, 70);
								}
							};

							var visible = mxUtils.indexOf(graph.hiddenTags, tag) < 0;
							var row = document.createElement('tr');
							var td = document.createElement('td');
							td.style.align = 'center';
							td.style.width = '16px';

							var img = document.createElement('img');
							img.setAttribute('src', visible ? Editor.visibleImage : Editor.hiddenImage);
							img.setAttribute('title', mxResources.get(visible ? 'hideIt' : 'show', [tag]));
							mxUtils.setOpacity(img, visible ? 75 : 25);
							img.className = 'geAdaptiveAsset';
							img.style.verticalAlign = 'middle';
							img.style.cursor = 'pointer';
							img.style.width = '16px';
							
							if (invert || Editor.isDarkMode())
							{
								img.style.filter = 'invert(100%)';
							}
							
							td.appendChild(img);

							mxEvent.addListener(img, 'click', function(evt)
							{
								if (mxEvent.isShiftDown(evt))
								{
									setAllVisible(mxUtils.indexOf(graph.hiddenTags, tag) >= 0);
								}
								else
								{
									graph.toggleHiddenTag(tag);
									removeInvisibleSelectionCells();
									graph.refresh();
								}

								mxEvent.consume(evt);
							});
							
							row.appendChild(td);

							td = document.createElement('td');
							td.style.align = 'center';
							td.style.width = '16px';

							var img = document.createElement('img');
							img.setAttribute('src', Editor.selectImage);
							img.setAttribute('title', mxResources.get('select'));
							mxUtils.setOpacity(img, visible ? 75 : 25);
							img.className = 'geAdaptiveAsset';
							img.style.verticalAlign = 'middle';
							img.style.cursor = 'pointer';
							img.style.width = '16px';
							
							if (invert || Editor.isDarkMode())
							{
								img.style.filter = 'invert(100%)';
							}

							mxEvent.addListener(img, 'click', function(evt)
							{
								setAllVisible(true);
								selectCells();	
								mxEvent.consume(evt);
							});
							
							td.appendChild(img);
							row.appendChild(td);

							td = document.createElement('td');
							td.style.overflow = 'hidden';
							td.style.whiteSpace = 'nowrap';
							td.style.textOverflow = 'ellipsis';
							td.style.verticalAlign = 'middle';
							td.style.cursor = 'pointer';
							td.setAttribute('title', tag);
	
							a = document.createElement('a');
							mxUtils.write(a, tag);
							a.style.textOverflow = 'ellipsis';
							a.style.position = 'relative';
							mxUtils.setOpacity(a, visible ? 100 : 40);
							td.appendChild(a);
	
							mxEvent.addListener(td, 'click', (function(evt)
							{
								if (mxEvent.isShiftDown(evt))
								{
									setAllVisible(true);
									selectCells();	
								}
								else
								{
									if (visible && graph.hiddenTags.length > 0)
									{
										setAllVisible(true);
									}
									else
									{
										setTagVisible();
									}
								}

								mxEvent.consume(evt);
							}));

							row.appendChild(td);

							if (graph.isEnabled())
							{
								td = document.createElement('td');
								td.style.verticalAlign = 'middle';
								td.style.textAlign = 'center';
								td.style.width = '18px';
	
								if (selected == null)
								{
									td.style.align = 'center';
									td.style.width = '16px';
		
									var img = document.createElement('img');
									img.setAttribute('src', Editor.trashImage);
									img.setAttribute('title', mxResources.get('removeIt', [tag]));
									mxUtils.setOpacity(img, visible ? 75 : 25);
									img.className = 'geAdaptiveAsset';
									img.style.verticalAlign = 'middle';
									img.style.cursor = 'pointer';
									img.style.width = '16px';

									if (invert || Editor.isDarkMode())
									{
										img.style.filter = 'invert(100%)';
									}

									mxEvent.addListener(img, 'click', function(evt)
									{
										var idx = mxUtils.indexOf(allTags, tag);

										if (idx >= 0)
										{
											allTags.splice(idx, 1);
										}

										graph.removeTagsForCells(
											graph.model.getDescendants(
											graph.model.getRoot()), [tag]);
										graph.refresh();

										mxEvent.consume(evt);
									});

									td.appendChild(img);
								}
								else
								{
									var cb2 = document.createElement('input');
									cb2.setAttribute('type', 'checkbox');
									cb2.style.margin = '0px';
		
									cb2.defaultChecked = (selected != null &&
										mxUtils.indexOf(selected, tag) >= 0);
									cb2.checked = cb2.defaultChecked;
									cb2.style.background = 'transparent';
									cb2.setAttribute('title', mxResources.get(
										cb2.defaultChecked ?
										'removeIt' : 'add', [tag]));
		
									mxEvent.addListener(cb2, 'change', function(evt)
									{
										if (cb2.checked)
										{
											graph.addTagsForCells(graph.getSelectionCells(), [tag]);
										}
										else
										{
											graph.removeTagsForCells(graph.getSelectionCells(), [tag]);
										}
									
										mxEvent.consume(evt);
									});
		
									td.appendChild(cb2);
								}

								row.appendChild(td);
							}
	
							tbody.appendChild(row);
						})(tags[i]);
					}
				}
	
				table.appendChild(tbody);
				tagCloud.appendChild(table);
			}
		};
	
		var refreshUi = mxUtils.bind(this, function(sender, evt)
		{
			if (isEnabled())
			{
				var tags = graph.getAllTags();
	
				for (var i = 0; i < tags.length; i++)
				{
					if (mxUtils.indexOf(allTags, tags[i]) < 0)
					{
						allTags.push(tags[i]);
					}
				}
	
				allTags.sort();
	
				if (graph.isSelectionEmpty())
				{
					refreshTags(allTags);
				}
				else
				{
					refreshTags(allTags, graph.getCommonTagsForCells(
						graph.getSelectionCells()));
				}
			}
		});
	
		graph.selectionModel.addListener(mxEvent.CHANGE, refreshUi);
		graph.model.addListener(mxEvent.CHANGE, refreshUi);
		graph.addListener(mxEvent.REFRESH, refreshUi);
	
		var footer = document.createElement('div');
		footer.className = 'geToolbarContainer geDialogToolbar';
		footer.style.position = 'absolute';
		footer.style.display = 'flex';
		footer.style.bottom = '0px';
		footer.style.left = '0px';
		footer.style.right = '0px';
		footer.style.height = '32px';
		footer.style.overflow = 'hidden';
		footer.style.padding = '3px 4px 4px 4px';
		footer.style.borderWidth = '1px 0px 0px 0px';
		footer.style.borderStyle = 'solid';
		footer.style.whiteSpace = 'nowrap';

		if (graph.isEnabled())
		{
			tagCloud.style.bottom = '32px';

			var link = document.createElement('a');
			link.className = 'geButton';

			var resetLink = link.cloneNode(false);
			resetLink.style.backgroundImage = 'url(' + Editor.visibleImage + ')';
			resetLink.setAttribute('title', mxResources.get('reset'));

			mxEvent.addListener(resetLink, 'click', function(evt)
			{
				graph.setHiddenTags([]);
	
				if (!mxEvent.isShiftDown(evt))
				{
					allTags = graph.hiddenTags.slice();
				}
	
				removeInvisibleSelectionCells();
				graph.refresh();
				mxEvent.consume(evt);
			});

			footer.appendChild(resetLink);
			
			if (addFn != null)
			{
				var addLink = link.cloneNode(false);
				addLink.style.backgroundImage = 'url(' + Editor.plusImage + ')';
				addLink.setAttribute('title', mxResources.get('add'));
			
				mxEvent.addListener(addLink, 'click', function(evt)
				{
					// Takes all tags and callback to update all tags
					addFn(allTags, function(newAllTags)
					{
						allTags = newAllTags;
						refreshUi();
					});

					mxEvent.consume(evt);
				});

				footer.appendChild(addLink);
			}
			
			div.appendChild(footer);
		}

		if (helpButton != null)
		{
			footer.appendChild(helpButton);
		}

		return {div: div, refresh: refreshUi};
	};
	
	/**
	 * Returns all custom fonts (old and new).
	 */
	Graph.prototype.getCustomFonts = function()
	{
		var fonts = this.extFonts;
		
		if (fonts != null)
		{
			fonts = fonts.slice();
		}
		else
		{
			fonts = [];
		}
		
		for (var key in Graph.customFontElements)
		{
			var font = Graph.customFontElements[key];
			fonts.push({name: font.name, url: font.url});
		}
		
		return fonts;
	};
	
	/**
	 * Assigns the given custom font to the selected text.
	 */
	Graph.prototype.setFont = function(name, url)
	{
		// Adds the font element to the document
		Graph.addFont(name, url);

		// Marks the element with a random font name so
		// that it can be found in the code below
		var temp = Editor.guid();
		document.execCommand('fontname', false, temp);

		// Finds the new or updated element and changes or
		// removes is data-font-src attribute as required
		var fonts = this.cellEditor.textarea.getElementsByTagName('font');
		var found = false;

		for (var i = 0; i < fonts.length; i++)
		{
			if (fonts[i].getAttribute('face') == temp)
			{
				found = true;
				fonts[i].setAttribute('face', name);

				if (url != null)
				{
					fonts[i].setAttribute('data-font-src', url);
				}
				else
				{
					fonts[i].removeAttribute('data-font-src');
				}
			}
		}

		if (!found)
		{
			// Fallback to use the real font name if a new
			// or updated element can not be found above
			document.execCommand('fontname', false, name);
		}
	};
	
	/**
	 * Disables fast zoom with shadow in lightbox for Safari
	 * to work around blank output on retina screen.
	 */
	var graphIsFastZoomEnabled = Graph.prototype.isFastZoomEnabled;
	
	Graph.prototype.isFastZoomEnabled = function()
	{
		return graphIsFastZoomEnabled.apply(this, arguments) && (!this.shadowVisible || !mxClient.IS_SF);
	};
	
	/**
	 * Updates the global variables from the vars URL parameter.
	 */
	Graph.prototype.updateGlobalUrlVariables = function()
	{
		this.globalVars = Editor.globalVars;
		
		if (urlParams['vars'] != null)
		{
			try
			{
				this.globalVars = (this.globalVars != null) ? mxUtils.clone(this.globalVars) : {};
				var vars = JSON.parse(decodeURIComponent(urlParams['vars']));
				
				if (vars != null)
				{
					for (var key in vars)
					{
						this.globalVars[key] = vars[key];
					}
				}
			}
			catch (e)
			{
				if (window.console != null)
				{
					console.log('Error in vars URL parameter: ' + e);
				}
			}
		}
	};

	/**
	 * Returns all global variables used for export. This function never returns null.
	 * This can be overridden by plugins to return global variables for export.
	 */
	Graph.prototype.getExportVariables = function()
	{
		return (this.globalVars != null) ? mxUtils.clone(this.globalVars) : {};
	};
	
	/**
	 * Adds support for vars URL parameter.
	 */
	var graphGetGlobalVariable = Graph.prototype.getGlobalVariable;
	
	Graph.prototype.getGlobalVariable = function(name)
	{
		var val = graphGetGlobalVariable.apply(this, arguments);
		
		if (val == null && this.globalVars != null)
		{
			val = this.globalVars[name];
		}
		
		return val;
	};

	/**
	 * Cached default stylesheet for image export in dark mode.
	 */
	Graph.prototype.getDefaultStylesheet = function()
	{
		if (this.defaultStylesheet == null)
		{
			var node = this.themes['default-style2'];
			var dec = new mxCodec(node.ownerDocument);
			this.defaultStylesheet = dec.decode(node);
		}
		
		return this.defaultStylesheet;
	};
	
	/**
	 * Overiddes function to use url parameter
	 */
	Graph.prototype.isViewer = function()
	{
		return urlParams['viewer'];
	};

	/**
	 * Temporarily overrides stylesheet during image export in dark mode.
	 */
	var graphGetSvg = Graph.prototype.getSvg;
	
	Graph.prototype.getSvg = function(background, scale, border, nocrop, crisp,
		ignoreSelection, showText, imgExport, linkTarget, hasShadow,
		incExtFonts, theme, exportType, cells, noCssClass, disableLinks)
	{
		var result = graphGetSvg.apply(this, arguments);

		if (theme != null)
		{
			// Uses style attribute to bypass removing fallback styles
			var style = result.getAttribute('style');
			
			if (style == null)
			{
				style = '';
			}

			var theme = (theme == 'auto' || this.getAdaptiveColors() == 'none') ?
				((this.getAdaptiveColors() == 'none') ?
					'light' : 'light dark') : theme;
			style += ' color-scheme: ' + theme + ';';
			result.setAttribute('style', style);
		}
		
		var extFonts = this.getCustomFonts();
		
		// Adds external fonts
		if (incExtFonts && extFonts.length > 0)
		{
			var svgDoc = result.ownerDocument;
			var style = (svgDoc.createElementNS != null) ?
		    	svgDoc.createElementNS(mxConstants.NS_SVG, 'style') : svgDoc.createElement('style');
			(svgDoc.setAttributeNS != null) ? style.setAttributeNS('type', 'text/css') :
				style.setAttribute('type', 'text/css');
			
			var prefix = '';
			var postfix = '';
			    	
			for (var i = 0; i < extFonts.length; i++)
			{
				var fontName = extFonts[i].name, fontUrl = extFonts[i].url;
				
				if (Graph.isCssFontUrl(fontUrl))
				{
					prefix += '@import url(' + Graph.rewriteGoogleFontUrl(fontUrl) + ');\n';
				}
				else
				{
					postfix += '@font-face {\n' +
			            'font-family: "' + fontName + '";\n' + 
			            'src: url("' + fontUrl + '");\n}\n';
				}				
			}
			
			style.appendChild(svgDoc.createTextNode(prefix + postfix));
			result.getElementsByTagName('defs')[0].appendChild(style);
		}

		// Converts background pages to subtrees
		if (Editor.replaceSvgDataUris && this.backgroundImage != null &&
			this.backgroundImage.originalSrc != null)
		{
			EditorUi.embedSvgImages(result);
		}

		// SVG element must be added to DOM for MathJax to work
		if (this.mathEnabled)
		{
			document.body.appendChild(result);
			Editor.MathJaxRender(result);
			result.parentNode.removeChild(result);

			// Copies MathJax CSS to output
			var style = result.ownerDocument.getElementById('MJX-SVG-styles');

			if (style != null)
			{
				result.getElementsByTagName('defs')[0].appendChild(style.cloneNode(true));
			}
		}
		
		return result;
	};
	
	/**
	 * Overridden to destroy the shape number.
	 */
	var cellRendererDestroy = mxCellRenderer.prototype.destroy;
	mxCellRenderer.prototype.destroy = function(state)
	{
		cellRendererDestroy.apply(this, arguments);
		
		if (state.secondLabel != null)
		{
			state.secondLabel.destroy();
			state.secondLabel = null;
		}
	};
	
	/**
	 * Includes the shape number in the return value.
	 */
	mxCellRenderer.prototype.getShapesForState = function(state)
	{
		return [state.shape, state.text, state.secondLabel, state.control];
	};

	/**
	 * Resets the global shape counter.
	 */
	var graphViewResetValidationState = mxGraphView.prototype.resetValidationState;
	
	mxGraphView.prototype.resetValidationState = function()
	{
		graphViewResetValidationState.apply(this, arguments);
		this.enumerationState = 0;
	};
	
	/**
	 * Adds shape number update the validation step.
	 */
	var graphViewStateValidated = mxGraphView.prototype.stateValidated;
	
	mxGraphView.prototype.stateValidated = function(state)
	{
		if (state.shape != null)
		{
			this.redrawEnumerationState(state);
		}

		return graphViewStateValidated.apply(this, arguments);
	};

	/**
	 * Returns the markup to be used for the enumeration shape.
	 */
	mxGraphView.prototype.createEnumerationValue = function(state)
	{
		var value = decodeURIComponent(mxUtils.getValue(state.style, 'enumerateValue', ''));

		if (value == '')
		{
			value = ++this.enumerationState;
		}

		return '<div style="padding:2px;border:1px solid gray;background:yellow;border-radius:2px;">' +
			mxUtils.htmlEntities(value) + '</div>';
	};

	/**
	 * Adds drawing and update of the shape number.
	 */
	mxGraphView.prototype.redrawEnumerationState = function(state)
	{
		var enumerate = mxUtils.getValue(state.style, 'enumerate', 0) == '1';

		if (enumerate && state.secondLabel == null)
		{
			state.secondLabel = new mxText('', new mxRectangle(),
				mxConstants.ALIGN_LEFT, mxConstants.ALIGN_BOTTOM);
			state.secondLabel.size = 12;
			state.secondLabel.state = state;
			state.secondLabel.dialect = mxConstants.DIALECT_STRICTHTML;

			this.graph.cellRenderer.initializeLabel(state, state.secondLabel);
		}
		else if (!enumerate && state.secondLabel != null)
		{
			state.secondLabel.destroy();
			state.secondLabel = null;
		}

		var shape = state.secondLabel;

		if (shape != null)
		{
			var s = state.view.scale;
			var value = this.createEnumerationValue(state);
			var bounds = this.graph.model.isVertex(state.cell) ?
				new mxRectangle(state.x + state.width - 4 * s, state.y + 4 * s, 0, 0) :
				mxRectangle.fromPoint(state.view.getPoint(state));

			if (!shape.bounds.equals(bounds) || shape.value != value || shape.scale != s)
			{
				shape.bounds = bounds;
				shape.value = value;
				shape.scale = s;
				shape.redraw();
			}
		}
	};

	/**
	 * Updates the SVG for the background image if it references another page.
	 */
	var graphRefresh = Graph.prototype.refresh;
	Graph.prototype.refresh = function()
	{
		this.refreshBackgroundImage();
		graphRefresh.apply(this, arguments);
	};
				 
	/**
	 * Updates the SVG for the background image if it references another page.
	 */
	Graph.prototype.refreshBackgroundImage = function()
	{
		if (this.backgroundImage != null && this.backgroundImage.originalSrc != null)
		{
			this.setBackgroundImage(this.backgroundImage);
			this.view.validateBackgroundImage();
		}
	};
	
	/**
	 * Sets default style (used in editor.get/setGraphXml below)
	 */
	var graphLoadStylesheet = Graph.prototype.loadStylesheet;
	
	Graph.prototype.loadStylesheet = function()
	{
		graphLoadStylesheet.apply(this, arguments);
		this.currentStyle = 'default-style2';
	};

	/**
	 * Adds support for data:action/json,{"actions":[actions]} where actions is
	 * a comma-separated list of JSON objects.
	 * 
	 * An example action is:
	 * 
	 * data:action/json,{"actions":[{"toggle": {"cells": ["3", "4"]}}]}
	 * 
	 * This toggles the visible state of the cells with ID 3 and 4.
	 */
	Graph.prototype.handleCustomLink = function(href, cell)
	{
		if (href.substring(0, 17) == 'data:action/json,')
		{
			var link = JSON.parse(href.substring(17));

			if (link.actions != null)
			{
				this.executeCustomActions(link.actions, null, cell);
			}
		}
	};
		
	/**
	 * Runs the given actions and invokes done when all actions have been executed.
	 * When adding new actions that reference cell IDs support for updating
	 * those cell IDs must be handled in Graph.updateCustomLinkActions
	 */
	Graph.prototype.executeCustomActions = function(actions, done, cell)
	{
		if (!this.executingCustomActions)
		{
			this.executingCustomActions = true;
			var updatingModel = false;
			var waitCounter = 0;
			var index = 0;

			var beginUpdate = mxUtils.bind(this, function()
			{
				if (!updatingModel)
				{
					updatingModel = true;
					this.model.beginUpdate();
				}
			});

			var endUpdate = mxUtils.bind(this, function()
			{
				if (updatingModel)
				{
					updatingModel = false;
					this.model.endUpdate();
				}
			});

			var waitAndExecute = mxUtils.bind(this, function()
			{
				if (waitCounter > 0)
				{
					waitCounter--;
				}

				if (waitCounter == 0)
				{
					executeNextAction()
				}
			});

			var executeNextAction = mxUtils.bind(this, function()
			{
				if (index < actions.length)
				{
					var stop = this.stoppingCustomActions;
					var action = actions[index++];
					var animations = [];

					// Executes open actions before starting transaction
					if (action.open != null)
					{
						endUpdate();

						if (this.isCustomLink(action.open))
						{
							if (!this.customLinkClicked(action.open, cell))
							{
								return;
							}
						}
						else
						{
							this.openLink(action.open);
						}
					}

					if (action.wait != null && !stop)
					{
						this.pendingExecuteNextAction = mxUtils.bind(this, function()
						{
							this.pendingExecuteNextAction = null;
							this.pendingWaitThread = null;
							waitAndExecute();
						});

						waitCounter++;
						this.pendingWaitThread = window.setTimeout(this.pendingExecuteNextAction,
							(action.wait != '') ? parseInt(action.wait) : 1000);
						endUpdate();
					}

					if (action.opacity != null && action.opacity.value != null)
					{
						Graph.setOpacityForNodes(this.getNodesForCells(
							this.getCellsForAction(action.opacity, true)),
							action.opacity.value);
					}

					if (action.fadeIn != null)
					{
						waitCounter++;
						Graph.fadeNodes(this.getNodesForCells(
							this.getCellsForAction(action.fadeIn, true)),
							0, 1, waitAndExecute, (stop) ?
							0 : action.fadeIn.delay);
					}

					if (action.fadeOut != null)
					{
						waitCounter++;
						Graph.fadeNodes(this.getNodesForCells(
							this.getCellsForAction(action.fadeOut, true)),
							1, 0, waitAndExecute, (stop) ?
							0 : action.fadeOut.delay);
					}

					if (action.wipeIn != null)
					{
						animations = animations.concat(this.createWipeAnimations(
							this.getCellsForAction(action.wipeIn, true), true));
					}

					if (action.wipeOut != null)
					{
						animations = animations.concat(this.createWipeAnimations(
							this.getCellsForAction(action.wipeOut, true), false));
					}

					// Executes all actions that change cell states
					if (action.toggle != null)
					{
						beginUpdate();
						this.toggleCells(this.getCellsForAction(action.toggle, true));
					}

					if (action.show != null)
					{
						beginUpdate();
						var temp = this.getCellsForAction(action.show, true);
						Graph.setOpacityForNodes(this.getNodesForCells(temp), 1);
						this.setCellsVisible(temp, true);
					}

					if (action.hide != null)
					{
						beginUpdate();
						var temp = this.getCellsForAction(action.hide, true);
						Graph.setOpacityForNodes(this.getNodesForCells(temp), 0);
						this.setCellsVisible(temp, false);
					}
					
					if (action.toggleStyle != null && action.toggleStyle.key != null)
					{
						beginUpdate();
						this.toggleCellStyles(action.toggleStyle.key, (action.toggleStyle.defaultValue != null) ?
							action.toggleStyle.defaultValue : '0', this.getCellsForAction(action.toggleStyle, true));
					}

					if (action.style != null && action.style.key != null)
					{
						beginUpdate();
						this.setCellStyles(action.style.key, action.style.value,
							this.getCellsForAction(action.style, true));
					}

					// Executes stateless actions on cells
					var cells = [];
						
					if (action.select != null && this.isEnabled())
					{
						cells = this.getCellsForAction(action.select);
						this.setSelectionCells(cells);
					}

					if (action.highlight != null)
					{
						cells = this.getCellsForAction(action.highlight);
						this.highlightCells(cells, action.highlight.color,
							action.highlight.duration,
							action.highlight.opacity);
					}

					if (action.scroll != null)
					{
						cells = this.getCellsForAction(action.scroll);
					}
					
					if (action.viewbox != null)
					{
						this.fitWindow(action.viewbox, action.viewbox.border);
					}
					
					if (cells.length > 0)
					{
						this.scrollCellToVisible(cells[0]);
					}
					
					if (cell != null && action.explore != null)
					{
						Graph.exploreFromCell(this, cell, action.explore);
					}

					if (action.tags != null)
					{
						if (action.tags.toggle != null)
						{
							var tags = action.tags.toggle;

							if (tags.length == 0)
							{
								tags = this.getAllTags();
							}

							for (var i = 0; i < tags.length; i++)
							{
								this.toggleHiddenTag(tags[i]);
							}
						}
						
						var hidden = null;

						if (action.tags.hidden != null)
						{
							if (hidden == null)
							{
								hidden = [];
							}
							
							hidden = hidden.concat(action.tags.hidden);
						}

						if (action.tags.visible != null)
						{
							if (hidden == null)
							{
								hidden = [];
							}

							var all = this.getAllTags();

							for (var i = 0; i < all.length; i++)
							{
								if (mxUtils.indexOf(action.tags.visible, all[i]) < 0 &&
									mxUtils.indexOf(hidden, all[i]) < 0)
								{
									hidden.push(all[i]);
								}
							}
						}

						if (hidden != null)
						{
							this.setHiddenTags(hidden);
						}

						this.refresh();
					}

					if (animations.length > 0)
					{
						waitCounter++;
						this.executeAnimations(animations, waitAndExecute,
							(stop) ? 1 : action.steps,
							(stop) ? 0 : action.delay);
					}

					if (waitCounter == 0)
					{
						executeNextAction();
					}
					else
					{
						endUpdate();
					}
				}
				else
				{
					this.executingCustomActions = false;
					this.stoppingCustomActions = false;
					endUpdate();

					if (done != null)
					{
						done();
					}
				}
			});

			executeNextAction();
		}
		else
		{
			this.stoppingCustomActions = true;

			if (this.pendingWaitThread != null)
			{
				window.clearTimeout(this.pendingWaitThread);
			}

			if (this.pendingExecuteNextAction != null)
			{
				this.pendingExecuteNextAction();
			}

			this.fireEvent(new mxEventObject('stopExecutingCustomActions'));
		}
	};

	/**
	 * Updates cell IDs in custom links on the given cell and its label.
	 */
	Graph.prototype.doUpdateCustomLinksForCell = function(mapping, cell)
	{
		var href = this.getLinkForCell(cell);
		
		if (href != null && href.substring(0, 17) == 'data:action/json,')
		{
			this.setLinkForCell(cell, this.updateCustomLink(mapping, href));
		}
		
		if (this.isHtmlLabel(cell))
		{
			var temp = document.createElement('div');
			temp.innerHTML = Graph.sanitizeHtml(this.getLabel(cell));
			var links = temp.getElementsByTagName('a');
			var changed = false;
			
			for (var i = 0; i < links.length; i++)
			{
				href = links[i].getAttribute('href');
				
				if (href != null && href.substring(0, 17) == 'data:action/json,')
				{
					links[i].setAttribute('href', this.updateCustomLink(mapping, href));
					changed = true;
				}
			}
			
			if (changed)
			{
				this.labelChanged(cell, temp.innerHTML);
			}
		}
	};
	
	/**
	 * Updates cell IDs in the given custom link and returns the updated link.
	 */
	Graph.prototype.updateCustomLink = function(mapping, href)
	{
		if (href.substring(0, 17) == 'data:action/json,')
		{
			try
			{
				var link = JSON.parse(href.substring(17));

				if (link.actions != null)
				{
					this.updateCustomLinkActions(mapping, link.actions);
					href = 'data:action/json,' + JSON.stringify(link);
				}
			}
			catch (e)
			{
				// Ignore
			}
		}
		
		return href;
	};

	/**
	 * Updates cell IDs in the given custom link actions.
	 */
	Graph.prototype.updateCustomLinkActions = function(mapping, actions)
	{
		for (var i = 0; i < actions.length; i++)
		{
			var action = actions[i];

			for (var name in action)
			{
				this.updateCustomLinkAction(mapping, action[name], 'cells');
				this.updateCustomLinkAction(mapping, action[name], 'excludeCells');
			}
		}
	};
	
	/**
	 * Updates cell IDs in the given custom link action.
	 */
	Graph.prototype.updateCustomLinkAction = function(mapping, action, name)
	{
		if (action != null && action[name] != null)
		{
			var result = [];
			
			for (var i = 0; i < action[name].length; i++)
			{
				if (action[name][i] == '*')
				{
					result.push(action[name][i]);
				}
				else
				{
					var temp = mapping[action[name][i]];
					
					if (temp != null)
					{
						if (temp != '')
						{
							result.push(temp);
						}
					}
					else
					{
						result.push(action[name][i]);
					}
				}
			}
			
			action[name] = result;
		}
	};
	
	/**
	 * Handles each action in the action array of a custom link. This code
	 * handles toggle actions for cell IDs.
	 */
	Graph.prototype.getCellsForAction = function(action, layers)
	{
		var result = this.getCellsById(action.cells).concat(
			this.getCellsForTags(action.tags, null, layers));

		// Removes excluded cells
		if (action.excludeCells != null)
		{
			var temp = [];

			for (var i = 0; i < result.length; i++)
			{
				if (action.excludeCells.indexOf(result[i].id) < 0)
				{
					temp.push(result[i]);
				}
			}

			result = temp;
		}

		return result;
	};
	
	/**
	 * Returns the cells in the model (or given array) that have all of the
	 * given tags in their tags property.
	 */
	Graph.prototype.getCellsById = function(ids)
	{
		var result = [];
		
		if (ids != null)
		{
			for (var i = 0; i < ids.length; i++)
			{
				if (ids[i] == '*')
				{
					var parent = this.model.getRoot();
					
					result = result.concat(this.model.filterDescendants(function(cell)
					{
						return cell != parent;
					}, parent));
				}
				else
				{
					var cell = this.model.getCell(ids[i]);
					
					if (cell != null)
					{
						result.push(cell);
					}
				}
			}
		}
		
		return result;
	};

	/**
	 * Adds support for custom fonts in cell styles.
	 */
	var graphIsCellVisible = Graph.prototype.isCellVisible;
	Graph.prototype.isCellVisible = function(cell)
	{
		return graphIsCellVisible.apply(this, arguments) &&
			!this.isAllTagsHidden(this.getTagsForCell(cell));
	};
  
	/**
	 * Returns the tags for the given cell as a string.
	 */
	Graph.prototype.setHiddenTags = function(tags)
	{
		this.hiddenTags = tags;
		this.fireEvent(new mxEventObject('hiddenTagsChanged'));
	};

	/**
	 * Returns the tags for the given cell as a string.
	 */
	Graph.prototype.toggleHiddenTag = function(tag)
	{
		var idx = mxUtils.indexOf(this.hiddenTags, tag);
									
		if (idx < 0)
		{
			this.hiddenTags.push(tag);
		}
		else if (idx >= 0)
		{
			this.hiddenTags.splice(idx, 1);
		}
		
		this.fireEvent(new mxEventObject('hiddenTagsChanged'));
	};
 
	/**
	 * Returns the cells in the model (or given array) that have all of the
	 * given tags in their tags property.
	 */
	Graph.prototype.isAllTagsHidden = function(tags)
	{
		if (tags == null || tags.length == 0 ||
			this.hiddenTags.length == 0)
		{
			return false;
		}
		else
		{
			var tmp = tags.split(' ');

			if (tmp.length > this.hiddenTags.length)
			{
				return false;
			}
			else
			{
				for (var i = 0; i < tmp.length; i++)
				{
					if (mxUtils.indexOf(this.hiddenTags, tmp[i]) < 0)
					{
						return false;
					}
				}
				
				return true;
			}
		}
	};

	/**
	 * Returns the cells in the model (or given array) that have all of the
	 * given tags in their tags property.
	 */
	Graph.prototype.getCellsForTags = function(tagList, cells, includeLayers, checkVisible)
	{
		var result = [];
		
		if (tagList != null)
		{
			cells = (cells != null) ? cells : this.model.getDescendants(this.model.getRoot());
			
			var tagCount = 0;
			var lookup = {};
			
			for (var i = 0; i < tagList.length; i++)
			{
				if (tagList[i].length > 0)
				{
					lookup[tagList[i]] = true;
					tagCount++;
				}
			}
			
			for (var i = 0; i < cells.length; i++)
			{
				if ((includeLayers && this.model.getParent(cells[i]) == this.model.root) ||
					this.model.isVertex(cells[i]) || this.model.isEdge(cells[i]))
				{
					var tags = this.getTagsForCell(cells[i]);
					var match = false;
	
					if (tags.length > 0)
					{
						var tmp = tags.split(' ');
						
						if (tmp.length >= tagList.length)
						{
							var matchCount = 0;
							
							for (var j = 0; j < tmp.length && (matchCount < tagCount); j++)
							{
								if (lookup[tmp[j]] != null)
								{
									matchCount++;
								}
							}
							
							match = matchCount == tagCount;
						}
					}
					
					if (match && ((checkVisible != true) || this.isCellVisible(cells[i])))
					{
						result.push(cells[i]);
					}
				}
			}
		}
		
		return result;
	};

	/**
	 * Returns all tags in the diagram.
	 */
	Graph.prototype.getAllTags = function()
	{
		return this.getTagsForCells(
			this.model.getDescendants(
				this.model.getRoot()));
		
	};

	/**
	 * Returns the common tags for the given cells as a array.
	 */
	Graph.prototype.getCommonTagsForCells = function(cells)
	{
		var commonTokens = null;
		var validTags = [];
		 
		for (var i = 0; i < cells.length; i++)
		{
			var tags = this.getTagsForCell(cells[i]);
			validTags = [];
 
			if (tags.length > 0)
			{
				var tokens = tags.split(' ');
				var temp = {};
				 
				for (var j = 0; j < tokens.length; j++)
				{
					if (commonTokens == null || commonTokens[tokens[j]] != null)
					{
						temp[tokens[j]] = true;
						validTags.push(tokens[j]);
					}
				}
				 
				commonTokens = temp;
			}
			else
			{
				return [];
			}
		}
	 
		return validTags;
	};
 
	/**
	 * Returns all tags for the given cells as an array.
	 */
	Graph.prototype.getTagsForCells = function(cells)
	{
		var tokens = [];
		var temp = {};
		
		for (var i = 0; i < cells.length; i++)
		{
			var tags = this.getTagsForCell(cells[i]);

			if (tags.length > 0)
			{
				var t = tags.split(' ');
				
				for (var j = 0; j < t.length; j++)
				{
					if (temp[t[j]] == null)
					{
						temp[t[j]] = true;
						tokens.push(t[j]);
					}
				}
			}
		}
		
		return tokens;
	};

	/**
	 * Returns the tags for the given cell as a string.
	 */
	Graph.prototype.getTagsForCell = function(cell)
	{
		return this.getAttributeForCell(cell, 'tags', '');
	};

	/**
	 * Adds the given array of tags to the given array cells.
	 */
	Graph.prototype.addTagsForCells = function(cells, tagList)
	{
		if (cells.length > 0 && tagList.length > 0)
		{
			this.model.beginUpdate();
			
			try
			{
				for (var i = 0; i < cells.length; i++)
				{
					var temp = this.getTagsForCell(cells[i]);
					var tags = temp.split(' ');
					var changed = false;
		
					for (var j = 0; j < tagList.length; j++)
					{
						var tag = mxUtils.trim(tagList[j]);

						if (tag != '' && mxUtils.indexOf(tags, tag) < 0)
						{
							temp = (temp.length > 0) ? temp + ' ' + tag : tag;
							changed = true;
						}
					}
					
					if (changed)
					{
						this.setAttributeForCell(cells[i], 'tags', temp);
					}
				}
			}
			finally
			{
				this.model.endUpdate();
			}
		}
	};

	/**
	 * Removes the given array of tags from the given array cells.
	 */
	Graph.prototype.removeTagsForCells = function(cells, tagList)
	{
		if (cells.length > 0 && tagList.length > 0)
		{
			this.model.beginUpdate();
			
			try
			{
				for (var i = 0; i < cells.length; i++)
				{
					var tags = this.getTagsForCell(cells[i]);
					
					if (tags.length > 0)
					{
						var tokens = tags.split(' ');
						var changed = false;
						
						for (var j = 0; j < tagList.length; j++)
						{
							var idx = mxUtils.indexOf(tokens, tagList[j]);
							
							if (idx >= 0)
							{
								tokens.splice(idx, 1);
								changed = true;
							}
						}

						if (changed)
						{
							this.setAttributeForCell(cells[i], 'tags', tokens.join(' '));
						}
					}
				}
			}
			finally
			{
				this.model.endUpdate();
			}
		}
	};
 
	/**
	 * Shows or hides the given cells.
	 */
	Graph.prototype.toggleCells = function(cells)
	{
		this.model.beginUpdate();
		try
		{
			for (var i = 0; i < cells.length; i++)
			{
				this.model.setVisible(cells[i], !this.model.isVisible(cells[i]))
			}
		}
		finally
		{
			this.model.endUpdate();
		}
	};
	
	/**
	 * Shows or hides the given cells.
	 */
	Graph.prototype.setCellsVisible = function(cells, visible)
	{
		this.model.beginUpdate();
		try
		{
			for (var i = 0; i < cells.length; i++)
			{
				this.model.setVisible(cells[i], visible);
			}
		}
		finally
		{
			this.model.endUpdate();
		}
	};
	
	/**
	 * Highlights the given cell.
	 */
	Graph.prototype.highlightCells = function(cells, color, duration, opacity)
	{
		for (var i = 0; i < cells.length; i++)
		{
			this.highlightCell(cells[i], color, duration, opacity);
		}
	};
	
	/**
	 * Highlights the given cell.
	 */
	Graph.prototype.highlightCell = function(cell, color, duration, opacity, strokeWidth)
	{
		color = (color != null) ? color : mxConstants.DEFAULT_VALID_COLOR;
		duration = (duration != null) ? duration : 1000;
		var state = this.view.getState(cell);
		var hl = null;
		
		if (state != null)
		{
			strokeWidth = (strokeWidth != null) ? strokeWidth : 4;
			var sw = Math.max(strokeWidth + 1, mxUtils.getValue(state.style,
				mxConstants.STYLE_STROKEWIDTH, 1) + strokeWidth);
			hl = new mxCellHighlight(this, color, sw, false);
			
			if (opacity != null)
			{
				hl.opacity = opacity;
			}
			
			hl.highlight(state);
			
			// Fades out the highlight after a duration
			window.setTimeout(function()
			{
				if (hl.shape != null)
				{
				 	mxUtils.setPrefixedStyle(hl.shape.node.style,
						'transition', 'all 1200ms ease-in-out');
					hl.shape.node.style.opacity = 0;
				}
				
				// Destroys the highlight after the fade
				window.setTimeout(function()
				{
					hl.destroy();
				}, 1200);
			}, duration);
		}

		return hl;
	};

	/**
	 * Adds a shadow filter to the given svg root.
	 */
	Graph.prototype.addSvgShadow = function(svgRoot, group, createOnly, extend)
	{
		createOnly = (createOnly != null) ? createOnly : false;
		extend = (extend != null) ? extend : true;
		var size = Math.ceil(this.svgShadowSize * this.svgShadowBlur);
		
		if (!createOnly)
		{
			group = (group != null) ? group : svgRoot.getElementsByTagName('g')[0];
			
			if (group != null)
			{
				this.updateShadowFilter(group, true);
				
				if (!isNaN(parseInt(svgRoot.getAttribute('width'))) && extend)
				{
					svgRoot.setAttribute('width', parseInt(svgRoot.getAttribute('width')) + size);
					svgRoot.setAttribute('height', parseInt(svgRoot.getAttribute('height')) + size);
					
					// Updates viewbox if one exists
					var vb = svgRoot.getAttribute('viewBox');
					
					if (vb != null && vb.length > 0)
					{
						var tokens = vb.split(' ');
						
						if (tokens.length > 3)
						{
							w = parseFloat(tokens[2]) + size;
							h = parseFloat(tokens[3]) + size;
							
							svgRoot.setAttribute('viewBox', tokens[0] + ' ' + tokens[1] + ' ' + w + ' ' + h);
						}
					}
				}
			}
		}

		return size;
	};
	
	/**
	 * Loads the stylesheet for this graph.
	 * LATER: Update shadow filter after zoom?
	 */
	Graph.prototype.updateShadowFilter = function(elt, visible)
	{
		if (!mxClient.IS_SF)
		{
			if (visible)
			{
				var cssColor = mxUtils.getLightDarkColor(
					this.svgShadowColor, this.svgShadowOpacity);
				elt.style.filter = 'drop-shadow(' +
					Math.round(this.svgShadowSize * 100) / 100 + 'px ' +
					Math.round(this.svgShadowSize * 100) / 100 + 'px ' +
					Math.round(this.svgShadowBlur * 100) / 100 + 'px ' +
					cssColor.cssText + ')'
			}
			else
			{
				elt.style.filter = '';
			}
		}
	};

	/**
	 * Loads the stylesheet for this graph.
	 */
	Graph.prototype.setShadowVisible = function(value, fireEvent)
	{
		fireEvent = (fireEvent != null) ? fireEvent : true;
		this.shadowVisible = value;
		this.updateShadowFilter(this.view.getDrawPane(), this.shadowVisible);
		
		if (fireEvent)
		{
			this.fireEvent(new mxEventObject('shadowVisibleChanged'));
		}
	};
	
	/**
	 * Selects first unlocked layer if one exists
	 */
	Graph.prototype.checkDefaultParent = function()
	{
		if (this.defaultParent != null &&
			!this.model.contains(this.defaultParent))
		{
			this.setDefaultParent(null);
			this.selectUnlockedLayer();
		}
	};
	
	/**
	 * Selects first unlocked layer if one exists
	 */
	Graph.prototype.selectUnlockedLayer = function()
	{
		if (this.defaultParent == null)
		{
			var childCount = this.model.getChildCount(this.model.root);
			var cell = null;
			var index = 0;
			
			do
			{
				cell = this.model.getChildAt(this.model.root, index);
			} while (index++ < childCount && mxUtils.getValue(this.getCellStyle(cell), 'locked', '0') == '1')
			
			if (cell != null)
			{
				this.setDefaultParent(cell);
			}
		}
	};

	/**
	 * Specifies special libraries that are loaded via dynamic JS. Add cases
	 * where the filename cannot be worked out from the package name. The
	 * standard scheme for this mapping is stencils/packagename.xml. If there
	 * are multiple XML files, any JS files or any anomalies in the filename or
	 * directory that contains the file, then an entry must be added here and
	 * in EmbedServlet2 for the loading of the shapes to work.
	 */
	// Required to avoid 404 for mockup.xml since naming of mxgraph.mockup.anchor does not contain
	// buttons even though it is defined in the mxMockupButtons.js file. This could only be fixed
	// with aliases for existing shapes or aliases for basenames, but this is essentially the same.
	mxStencilRegistry.libraries['mockup'] = [SHAPES_PATH + '/mockup/mxMockupButtons.js'];
	
	mxStencilRegistry.libraries['arrows2'] = [SHAPES_PATH + '/mxArrows.js'];
	mxStencilRegistry.libraries['atlassian'] = [STENCIL_PATH + '/atlassian.xml', SHAPES_PATH + '/mxAtlassian.js'];
	mxStencilRegistry.libraries['bpmn'] = [SHAPES_PATH + '/mxBasic.js', STENCIL_PATH + '/bpmn.xml', SHAPES_PATH + '/bpmn/mxBpmnShape2.js'];
	mxStencilRegistry.libraries['bpmn2'] = [SHAPES_PATH + '/mxBasic.js', STENCIL_PATH + '/bpmn.xml', SHAPES_PATH + '/bpmn/mxBpmnShape2.js'];
	mxStencilRegistry.libraries['c4'] = [SHAPES_PATH + '/mxC4.js'];
	mxStencilRegistry.libraries['cisco19'] = [SHAPES_PATH + '/mxCisco19.js', STENCIL_PATH + '/cisco19.xml'];
	mxStencilRegistry.libraries['cisco_safe'] = [SHAPES_PATH + '/mxCiscoSafe.js', STENCIL_PATH + '/cisco_safe/architecture.xml', STENCIL_PATH + '/cisco_safe/business_icons.xml', STENCIL_PATH + '/cisco_safe/capability.xml', STENCIL_PATH + '/cisco_safe/design.xml', STENCIL_PATH + '/cisco_safe/iot_things_icons.xml', STENCIL_PATH + '/cisco_safe/people_places_things_icons.xml', STENCIL_PATH + '/cisco_safe/security_icons.xml', STENCIL_PATH + '/cisco_safe/technology_icons.xml', STENCIL_PATH + '/cisco_safe/threat.xml'];
	mxStencilRegistry.libraries['dfd'] = [SHAPES_PATH + '/mxDFD.js'];
	mxStencilRegistry.libraries['er'] = [SHAPES_PATH + '/er/mxER.js'];
	mxStencilRegistry.libraries['kubernetes'] = [SHAPES_PATH + '/mxKubernetes.js', STENCIL_PATH + '/kubernetes.xml', STENCIL_PATH + '/kubernetes2.xml'];
	mxStencilRegistry.libraries['flowchart'] = [SHAPES_PATH + '/mxFlowchart.js', STENCIL_PATH + '/flowchart.xml'];
	mxStencilRegistry.libraries['ios'] = [SHAPES_PATH + '/mockup/mxMockupiOS.js'];
	mxStencilRegistry.libraries['rackGeneral'] = [SHAPES_PATH + '/rack/mxRack.js', STENCIL_PATH + '/rack/general.xml'];
	mxStencilRegistry.libraries['rackF5'] = [STENCIL_PATH + '/rack/f5.xml'];
	mxStencilRegistry.libraries['lean_mapping'] = [SHAPES_PATH + '/mxLeanMap.js', STENCIL_PATH + '/lean_mapping.xml'];
	mxStencilRegistry.libraries['basic'] = [SHAPES_PATH + '/mxBasic.js', STENCIL_PATH + '/basic.xml'];
	mxStencilRegistry.libraries['ios7icons'] = [STENCIL_PATH + '/ios7/icons.xml'];
	mxStencilRegistry.libraries['ios7ui'] = [SHAPES_PATH + '/ios7/mxIOS7Ui.js', STENCIL_PATH + '/ios7/misc.xml'];
	mxStencilRegistry.libraries['android'] = [SHAPES_PATH + '/mxAndroid.js', STENCIL_PATH + '/android/android.xml'];
	mxStencilRegistry.libraries['electrical/abstract'] = [SHAPES_PATH + '/mxElectrical.js', STENCIL_PATH + '/electrical/abstract.xml'];
	mxStencilRegistry.libraries['electrical/logic_gates'] = [SHAPES_PATH + '/mxElectrical.js', STENCIL_PATH + '/electrical/logic_gates.xml'];
	mxStencilRegistry.libraries['electrical/miscellaneous'] = [SHAPES_PATH + '/mxElectrical.js', STENCIL_PATH + '/electrical/miscellaneous.xml'];
	mxStencilRegistry.libraries['electrical/signal_sources'] = [SHAPES_PATH + '/mxElectrical.js', STENCIL_PATH + '/electrical/signal_sources.xml'];
	mxStencilRegistry.libraries['electrical/electro-mechanical'] = [SHAPES_PATH + '/mxElectrical.js', STENCIL_PATH + '/electrical/electro-mechanical.xml'];
	mxStencilRegistry.libraries['electrical/transmission'] = [SHAPES_PATH + '/mxElectrical.js', STENCIL_PATH + '/electrical/transmission.xml'];
	mxStencilRegistry.libraries['infographic'] = [SHAPES_PATH + '/mxInfographic.js'];
	mxStencilRegistry.libraries['mockup/buttons'] = [SHAPES_PATH + '/mockup/mxMockupButtons.js'];
	mxStencilRegistry.libraries['mockup/containers'] = [SHAPES_PATH + '/mockup/mxMockupContainers.js'];
	mxStencilRegistry.libraries['mockup/forms'] = [SHAPES_PATH + '/mockup/mxMockupForms.js'];
	mxStencilRegistry.libraries['mockup/graphics'] = [SHAPES_PATH + '/mockup/mxMockupGraphics.js', STENCIL_PATH + '/mockup/misc.xml'];
	mxStencilRegistry.libraries['mockup/markup'] = [SHAPES_PATH + '/mockup/mxMockupMarkup.js'];
	mxStencilRegistry.libraries['mockup/misc'] = [SHAPES_PATH + '/mockup/mxMockupMisc.js', STENCIL_PATH + '/mockup/misc.xml'];
	mxStencilRegistry.libraries['mockup/navigation'] = [SHAPES_PATH + '/mockup/mxMockupNavigation.js', STENCIL_PATH + '/mockup/misc.xml'];
	mxStencilRegistry.libraries['mockup/text'] = [SHAPES_PATH + '/mockup/mxMockupText.js'];
	mxStencilRegistry.libraries['floorplan'] = [SHAPES_PATH + '/mxFloorplan.js', STENCIL_PATH + '/floorplan.xml'];
	mxStencilRegistry.libraries['bootstrap'] = [SHAPES_PATH + '/mxBootstrap.js', SHAPES_PATH + '/mxBasic.js', STENCIL_PATH + '/bootstrap.xml'];
	mxStencilRegistry.libraries['gmdl'] = [SHAPES_PATH + '/mxGmdl.js', STENCIL_PATH + '/gmdl.xml'];
	mxStencilRegistry.libraries['gcp2'] = [SHAPES_PATH + '/mxGCP2.js', STENCIL_PATH + '/gcp2.xml'];
	mxStencilRegistry.libraries['ibm'] = [SHAPES_PATH + '/mxIBM.js', STENCIL_PATH + '/ibm.xml'];
	mxStencilRegistry.libraries['ibmcloud'] = [STENCIL_PATH + '/ibm_cloud.xml'];
	mxStencilRegistry.libraries['cabinets'] = [SHAPES_PATH + '/mxCabinets.js', STENCIL_PATH + '/cabinets.xml'];
	mxStencilRegistry.libraries['archimate'] = [SHAPES_PATH + '/mxArchiMate.js'];
	mxStencilRegistry.libraries['archimate3'] = [SHAPES_PATH + '/mxArchiMate3.js'];
	mxStencilRegistry.libraries['sysml'] = [SHAPES_PATH + '/mxSysML.js'];
	mxStencilRegistry.libraries['eip'] = [SHAPES_PATH + '/mxEip.js', STENCIL_PATH + '/eip.xml'];
	mxStencilRegistry.libraries['networks'] = [SHAPES_PATH + '/mxNetworks.js', STENCIL_PATH + '/networks.xml'];
	mxStencilRegistry.libraries['networks2'] = [SHAPES_PATH + '/mxNetworks2.js', STENCIL_PATH + '/networks2.xml'];
	mxStencilRegistry.libraries['aws3d'] = [SHAPES_PATH + '/mxAWS3D.js', STENCIL_PATH + '/aws3d.xml'];
	mxStencilRegistry.libraries['aws4'] = [SHAPES_PATH + '/mxAWS4.js', STENCIL_PATH + '/aws4.xml'];
	mxStencilRegistry.libraries['aws4b'] = [SHAPES_PATH + '/mxAWS4.js', STENCIL_PATH + '/aws4.xml'];
	mxStencilRegistry.libraries['uml25'] = [SHAPES_PATH + '/mxUML25.js'];
	mxStencilRegistry.libraries['veeam'] = [STENCIL_PATH + '/veeam/2d.xml', STENCIL_PATH + '/veeam/3d.xml', STENCIL_PATH + '/veeam/veeam.xml'];
	mxStencilRegistry.libraries['veeam2'] = [STENCIL_PATH + '/veeam/2d.xml', STENCIL_PATH + '/veeam/3d.xml', STENCIL_PATH + '/veeam/veeam2.xml'];
	mxStencilRegistry.libraries['pid2inst'] = [SHAPES_PATH + '/pid2/mxPidInstruments.js'];
	mxStencilRegistry.libraries['pid2misc'] = [SHAPES_PATH + '/pid2/mxPidMisc.js', STENCIL_PATH + '/pid/misc.xml'];
	mxStencilRegistry.libraries['pid2valves'] = [SHAPES_PATH + '/pid2/mxPidValves.js'];
	mxStencilRegistry.libraries['pidFlowSensors'] = [STENCIL_PATH + '/pid/flow_sensors.xml'];
	mxStencilRegistry.libraries['salesforce'] = [STENCIL_PATH + '/salesforce.xml'];
	mxStencilRegistry.libraries['sap'] = [SHAPES_PATH + '/mxSAP.js', STENCIL_PATH + '/sap.xml'];
	mxStencilRegistry.libraries['emoji'] = [SHAPES_PATH + '/emoji/mxEmoji.js'];

	// Triggers dynamic loading for markers
	mxMarker.getPackageForType = function(type)
	{
		var name = null;
		
		if (type != null && type.length > 0)
		{
			if (type.substring(0, 2) == 'ER')
			{
				name = 'mxgraph.er';
			}
			else if (type.substring(0, 5) == 'sysML')
			{
				name = 'mxgraph.sysml';
			}
		}
		
		return name;
	};
	
	var mxMarkerCreateMarker = mxMarker.createMarker;
	
	mxMarker.createMarker = function(canvas, shape, type, pe, unitX, unitY, size, source, sw, filled)
	{
		if (type != null)
		{
			var f = mxMarker.markers[type];
			
			if (f == null)
			{
				var name = this.getPackageForType(type);
				
				if (name != null)
				{
					mxStencilRegistry.getStencil(name);
				}
			}
		}
		
		return mxMarkerCreateMarker.apply(this, arguments);
	};

	/**
	 * Adds style to mark stencils as lines.
	 */
	var mxStencilDrawShape = mxStencil.prototype.drawShape;
		
	mxStencil.prototype.drawShape = function(canvas, shape, x, y, w, h)
	{
		if (mxUtils.getValue(shape.style, 'lineShape', null) == '1')
		{
			canvas.setFillColor(mxUtils.getValue(shape.style,
				mxConstants.STYLE_STROKECOLOR, this.stroke));
		}

		return mxStencilDrawShape.apply(this, arguments);
	};

	/**
	 * Constructs a new print dialog.
	 */
	PrintDialog.prototype.create = function(editorUi, titleText, fn, btnTitle)
	{
		var graph = editorUi.editor.graph;
		var div = document.createElement('div');
		
		var title = document.createElement('h3');
		title.style.width = '100%';
		title.style.textAlign = 'center';
		title.style.marginTop = '0px';
		mxUtils.write(title, titleText || mxResources.get('print'));
		div.appendChild(title);

		var currentPage = 1;
		var pageCount = 1;

		// Pages
		var pagesSection = document.createElement('div');
		pagesSection.style.whiteSpace = 'nowrap';
		
		var allPagesRadio = document.createElement('input');
		allPagesRadio.style.marginRight = '8px';
		allPagesRadio.style.marginBottom = '8px';
		allPagesRadio.setAttribute('type', 'radio');
		allPagesRadio.setAttribute('name', 'pages-printdialog');
		
		pagesSection.appendChild(allPagesRadio);

		var span = document.createElement('span');
		mxUtils.write(span, mxResources.get('allPages'));
		mxEvent.addListener(span, 'click', function()
		{
			allPagesRadio.checked = true;
		});
		pagesSection.appendChild(span);

		mxUtils.br(pagesSection);

		// Page range
		var pagesRadio = allPagesRadio.cloneNode(true);
		pagesSection.appendChild(pagesRadio);
		
		var span = document.createElement('span');
		mxUtils.write(span, mxResources.get('pages') + ':');
		pagesSection.appendChild(span);
		mxEvent.addListener(span, 'click', function()
		{
			pagesRadio.checked = true;
		});
		
		var pagesFromInput = document.createElement('input');
		pagesFromInput.style.margin = '0 4px';
		pagesFromInput.setAttribute('value', '1');
		pagesFromInput.setAttribute('type', 'number');
		pagesFromInput.setAttribute('min', '1');
		pagesFromInput.style.width = '40px';
		pagesSection.appendChild(pagesFromInput);
		
		var span = document.createElement('span');
		mxUtils.write(span, mxResources.get('to'));
		pagesSection.appendChild(span);
		
		var pagesToInput = pagesFromInput.cloneNode(true);
		pagesSection.appendChild(pagesToInput);

		mxEvent.addListener(pagesFromInput, 'focus', function()
		{
			pagesRadio.checked = true;
		});
		
		mxEvent.addListener(pagesToInput, 'focus', function()
		{
			pagesRadio.checked = true;
		});
		
		function validatePageRange()
		{
			pagesToInput.value = Math.max(1, Math.min(pageCount,
				Math.max(parseInt(pagesToInput.value), parseInt(pagesFromInput.value))));
			pagesFromInput.value = Math.max(1, Math.min(pageCount,
				Math.min(parseInt(pagesToInput.value), parseInt(pagesFromInput.value))));
		};
		
		mxEvent.addListener(pagesFromInput, 'change', validatePageRange);
		mxEvent.addListener(pagesToInput, 'change', validatePageRange);
		
		if (editorUi.pages != null)
		{
			pageCount = editorUi.pages.length;

			if (editorUi.currentPage != null)
			{
				for (var i = 0; i < pageCount; i++)
				{
					if (editorUi.currentPage == editorUi.pages[i])
					{
						currentPage = i + 1;
						break;
					}
				}
			}
		}
		
		if (editorUi.lastPrintPagesFromInput != null &&
			editorUi.lastPrintPagesToInput != null)
		{
			pagesFromInput.value = Math.min(pageCount,
				Math.max(1, editorUi.lastPrintPagesFromInput));
			pagesToInput.value = Math.min(pageCount,
				Math.max(1, editorUi.lastPrintPagesToInput));
		}
		else
		{
			pagesFromInput.value = currentPage;
			pagesToInput.value = currentPage;
		}
		
		pagesFromInput.setAttribute('max', pageCount);
		pagesToInput.setAttribute('max', pageCount);

		var currPage = mxUtils.button(mxResources.get('currentPage'), function(evt)
		{
			pagesFromInput.value = currentPage;
			pagesToInput.value = currentPage;
			pagesRadio.checked = true;
		});

		currPage.setAttribute('title', mxResources.get('currentPage'));
		currPage.style.marginLeft = '4px';
		currPage.style.maxWidth = '100px';
		currPage.style.overflow = 'hidden';
		currPage.style.textOverflow = 'ellipsis';
		currPage.style.whiteSpace = 'nowrap';

		pagesSection.appendChild(currPage);
		
		if (pageCount > 1)
		{
			div.appendChild(pagesSection);
		}
		
		// Selection only
		var selectionSection = document.createElement('div');
		selectionSection.style.borderBottom = '1px solid lightGray';
		selectionSection.style.paddingBottom = '12px';
		selectionSection.style.marginBottom = '12px';
		selectionSection.style.whiteSpace = 'nowrap';

		var selectionOnlyRadio = document.createElement('input');
		selectionOnlyRadio.setAttribute('name', 'pages-printdialog');
		selectionOnlyRadio.setAttribute('type', (pageCount == 1) ? 'checkbox' : 'radio');
		selectionOnlyRadio.style.marginRight = '8px';
		
		if (graph.isSelectionEmpty())
		{
			selectionOnlyRadio.setAttribute('disabled', 'disabled');
		}

		if (graph.isEnabled())
		{
			selectionSection.appendChild(selectionOnlyRadio);
		
			var span = document.createElement('span');
			mxUtils.write(span, mxResources.get('selectionOnly'));
			selectionSection.appendChild(span);
		}

		if (graph.isEnabled() || pageCount > 1)
		{
			div.appendChild(selectionSection);
		}

		if (!editorUi.isPagesEnabled() || editorUi.lastPrintPagesRadioChecked)
		{
			pagesRadio.checked = true;
		}
		else if (!graph.isSelectionEmpty() && editorUi.lastPrintSelectionOnlyChecked)
		{
			selectionOnlyRadio.checked = true;
		}
		else
		{
			allPagesRadio.checked = true;
		}

		if (!graph.isSelectionEmpty())
		{
			mxEvent.addListener(span, 'click', function()
			{
				selectionOnlyRadio.checked = !selectionOnlyRadio.checked;
			});
		}
		
		// Page view
		var pageViewSection = document.createElement('div');
		pageViewSection.style.whiteSpace = 'nowrap';

		var pageViewRadio = document.createElement('input');
		pageViewRadio.style.marginBottom = '8px';
		pageViewRadio.style.marginRight = '8px';
		pageViewRadio.setAttribute('type', 'radio');
		pageViewRadio.setAttribute('name', 'printSize');
		pageViewSection.appendChild(pageViewRadio);

		var span = document.createElement('span');
		mxUtils.write(span, mxResources.get('pageView'));
		pageViewSection.appendChild(span);
		mxEvent.addListener(pageViewSection, 'click', function()
		{
			pageViewRadio.checked = true;
		});

		div.appendChild(pageViewSection);
		
		// Crop
		var cropSection = document.createElement('div');
		cropSection.style.whiteSpace = 'nowrap';

		var cropRadio = document.createElement('input');
		cropRadio.style.marginBottom = '8px';
		cropRadio.style.marginRight = '8px';
		cropRadio.setAttribute('type', 'radio');
		cropRadio.setAttribute('name', 'printSize');
		cropSection.appendChild(cropRadio);

		var span = document.createElement('span');
		mxUtils.write(span, mxResources.get('crop'));
		cropSection.appendChild(span);
		mxEvent.addListener(cropSection, 'click', function()
		{
			cropRadio.checked = true;
		});

		div.appendChild(cropSection);

		// Fit to ...
		var fitSection = document.createElement('div');
		fitSection.style.whiteSpace = 'nowrap';

		var fitRadio = document.createElement('input');
		fitRadio.style.marginBottom = '8px';
		fitRadio.style.marginRight = '8px';
		fitRadio.setAttribute('type', 'radio');
		fitRadio.setAttribute('name', 'printSize');
		
		var spanFitRadio = document.createElement('div');
		spanFitRadio.style.display = 'inline-block';
		spanFitRadio.style.verticalAlign = 'top';
		spanFitRadio.style.paddingTop = '2px';
		spanFitRadio.appendChild(fitRadio);
		fitSection.appendChild(spanFitRadio);
		
		var table = document.createElement('table');
		table.style.display = 'inline-block';
		table.style.borderSpacing = '0';
		var tbody = document.createElement('tbody');
		
		var row1 = document.createElement('tr');
		var row2 = row1.cloneNode(true);
		
		var td1 = document.createElement('td');
		var td2 = td1.cloneNode(true);
		var td3 = td1.cloneNode(true);
		
		var td4 = td1.cloneNode(true);
		var td5 = td1.cloneNode(true);
		var td6 = td1.cloneNode(true);
		
		td1.style.textAlign = 'right';
		td4.style.textAlign = 'right';

		mxUtils.write(td1, mxResources.get('fitTo'));
		mxEvent.addListener(fitSection, 'click', function(e)
		{
			if (mxEvent.getSource(e) != fitRadio)
			{
				fitRadio.checked = !fitRadio.checked;
			}
		});
		
		var sheetsAcrossInput = document.createElement('input');
		sheetsAcrossInput.style.margin = '0 2px';
		sheetsAcrossInput.style.boxSizing = 'border-box';
		sheetsAcrossInput.setAttribute('value', editorUi.lastPrintSheetsAcross || '1');
		sheetsAcrossInput.setAttribute('min', '1');
		sheetsAcrossInput.setAttribute('type', 'number');
		sheetsAcrossInput.style.width = '40px';
		td2.appendChild(sheetsAcrossInput);
		
		var span = document.createElement('span');
		mxUtils.write(span, mxResources.get('fitToSheetsAcross'));
		td3.appendChild(span);

		mxUtils.write(td4, mxResources.get('fitToBy'));
		
		var sheetsDownInput = sheetsAcrossInput.cloneNode(true);
		sheetsDownInput.setAttribute('value', editorUi.lastPrintSheetsDown || '1');
		td5.appendChild(sheetsDownInput);
		
		mxEvent.addListener(sheetsAcrossInput, 'click', function(e)
		{
			fitRadio.checked = true;
			mxEvent.consume(e);
		});

		mxEvent.addListener(sheetsDownInput, 'click', function(e)
		{
			fitRadio.checked = true;
			mxEvent.consume(e);
		});

		if (editorUi.lastPrintCropRadioChecked)
		{
			cropRadio.checked = true;
		}
		else if (editorUi.lastPrintFitRadioChecked)
		{
			fitRadio.checked = true;
		}
		else
		{
			pageViewRadio.checked = true;
		}

		var span = document.createElement('span');
		mxUtils.write(span, mxResources.get('fitToSheetsDown'));
		td6.appendChild(span);
		
		row1.appendChild(td1);
		row1.appendChild(td2);
		row1.appendChild(td3);
		
		row2.appendChild(td4);
		row2.appendChild(td5);
		row2.appendChild(td6);
		
		tbody.appendChild(row1);
		tbody.appendChild(row2);
		table.appendChild(tbody);
		fitSection.appendChild(table);
		
		div.appendChild(fitSection);

		// Border and zoom
		var optionsSection = document.createElement('div');
		optionsSection.style.borderTop = '1px solid lightGray';
		optionsSection.style.whiteSpace = 'nowrap';
		optionsSection.style.paddingTop = '12px';
		optionsSection.style.marginTop = '12px';
		optionsSection.style.paddingLeft = '8px';
		
		mxUtils.write(optionsSection, mxResources.get('borderWidth') + ':');
		var borderInput = document.createElement('input');
		borderInput.setAttribute('type', 'number');
		borderInput.setAttribute('min', '0');
		borderInput.style.width = '40px';
		borderInput.style.marginLeft = '4px';
		borderInput.value = (editorUi.lastPrintBorder != null) ?
			editorUi.lastPrintBorder : mxPrintPreview.prototype.pageMargin;
		optionsSection.appendChild(borderInput);

		var span = document.createElement('span');
		span.style.marginLeft = '8px';
		mxUtils.write(span, mxResources.get('zoom') + ':');
		optionsSection.appendChild(span);
		
		var zoomInput = document.createElement('input');
		zoomInput.style.width = '60px';
		zoomInput.style.marginLeft = '4px';
		zoomInput.value = (editorUi.lastPrintZoom != null) ?
			editorUi.lastPrintZoom : '100%';
		optionsSection.appendChild(zoomInput);

		mxUtils.br(optionsSection);

		// Grid
		var gridInput = document.createElement('input');
		gridInput.setAttribute('type', 'checkbox');
		gridInput.style.marginTop = '12px';
		gridInput.checked = (editorUi.lastPrintGrid != null) ?
			editorUi.lastPrintGrid : false;
		optionsSection.appendChild(gridInput);

		var span = document.createElement('span');
		span.style.marginLeft = '4px';
		span.style.marginRight = '8px';
		mxUtils.write(span, mxResources.get('grid'));
		optionsSection.appendChild(span);

		mxEvent.addListener(span, 'click', function(e)
		{
			gridInput.checked = true;
			mxEvent.consume(e);
		});
		
		// Shadows enabled
		var shadowsInput = document.createElement('input');
		shadowsInput.setAttribute('type', 'checkbox');
		shadowsInput.style.marginTop = '12px';
		shadowsInput.checked = (editorUi.lastPrintShadow != null) ?
			editorUi.lastPrintShadow : false;
		optionsSection.appendChild(shadowsInput);

		var span = document.createElement('span');
		span.style.marginLeft = '4px';
		span.style.marginRight = '8px';
		mxUtils.write(span, mxResources.get('shadows'));
		optionsSection.appendChild(span);

		if (!editorUi.isOffline() || mxClient.IS_CHROMEAPP)
		{
			span.appendChild(editorUi.createHelpIcon(
				'https://github.com/jgraph/drawio/discussions/5136'));
		}

		mxEvent.addListener(span, 'click', function(e)
		{
			if (mxEvent.getSource(e).nodeName != 'IMG')
			{
				shadowsInput.checked = true;
				mxEvent.consume(e);
			}
		});

		// Hides shadows option if not supported
		if (!Editor.enableShadowOption)
		{
			shadowsInput.style.display = 'none';
			span.style.display = 'none';
		}
		else if (fn != null)
		{
			mxUtils.br(optionsSection);
		}

		// Transparent background
		var transparentInput = document.createElement('input');
		transparentInput.setAttribute('type', 'checkbox');
		transparentInput.style.marginTop = '10px';
		transparentInput.checked = (editorUi.lastPrintTransparent != null) ?
			editorUi.lastPrintTransparent : false;

		// Export
		if (fn != null)
		{
			optionsSection.appendChild(transparentInput);

			var span = document.createElement('span');
			span.style.marginLeft = '4px';
			mxUtils.write(span, mxResources.get('transparentBackground'));
			optionsSection.appendChild(span);

			mxEvent.addListener(span, 'click', function(e)
			{
				transparentInput.checked = true;
				mxEvent.consume(e);
			});
		}

		// Include diagram
		var includeInput = document.createElement('input');
		includeInput.setAttribute('type', 'checkbox');
		includeInput.style.marginTop = '10px';
		includeInput.checked = (editorUi.lastPrintInclude != null) ?
			editorUi.lastPrintInclude : Editor.defaultIncludeDiagram;

		if (fn != null && !mxClient.IS_CHROMEAPP &&
			editorUi.getServiceName() == 'draw.io')
		{
			mxUtils.br(optionsSection);
			optionsSection.appendChild(includeInput);

			var span = document.createElement('span');
			span.style.marginLeft = '4px';
			mxUtils.write(span, mxResources.get('includeCopyOfMyDiagram'));
			optionsSection.appendChild(span);

			mxEvent.addListener(span, 'click', function(e)
			{
				includeInput.checked = true;
				mxEvent.consume(e);
			});
		}


		div.appendChild(optionsSection);

		// Buttons
		var buttons = document.createElement('div');
		buttons.style.marginTop = '30px';
		buttons.style.textAlign = 'right';
		buttons.style.whiteSpace = 'nowrap';
		
		// Preview or print
		function preview(print)
		{
			editorUi.lastPrintPagesRadioChecked = pagesRadio.checked;
			editorUi.lastPrintSelectionOnlyChecked = selectionOnlyRadio.checked;
			editorUi.lastPrintCropRadioChecked = cropRadio.checked;
			editorUi.lastPrintFitRadioChecked = fitRadio.checked;

			if (pagesRadio.checked && pagesFromInput.value < pagesToInput.value)
			{
				editorUi.lastPrintPagesFromInput = pagesFromInput.value;
				editorUi.lastPrintPagesToInput = pagesToInput.value;
			}
			else
			{
				editorUi.lastPrintPagesFromInput = null;
				editorUi.lastPrintPagesToInput = null;
			}

			zoomInput.value = Math.max(1, Math.min(1600, parseInt(zoomInput.value))) + '%';
			editorUi.lastPrintZoom = zoomInput.value;
			editorUi.lastPrintBorder = borderInput.value;
			editorUi.lastPrintGrid = gridInput.checked;
			editorUi.lastPrintTransparent = transparentInput.checked;
			editorUi.lastPrintInclude = includeInput.checked;
			editorUi.lastPrintSheetsAcross = sheetsAcrossInput.value;
			editorUi.lastPrintSheetsDown = sheetsDownInput.value;

			var args = {};
			args.border = parseInt(borderInput.value);
			args.scale = parseInt(zoomInput.value) / 100;
			args.grid = gridInput.checked;
			args.shadows = shadowsInput.checked;
			args.transparent = transparentInput.checked;
			args.includeCopy = includeInput.checked;
			args.sheetsAcross = parseInt(sheetsAcrossInput.value);
			args.sheetsDown = parseInt(sheetsDownInput.value);
			args.pagesFrom = parseInt(pagesFromInput.value);
			args.pagesTo = parseInt(pagesToInput.value);
			args.allPages = allPagesRadio.checked;
			args.selection = selectionOnlyRadio.checked;
			args.pageView = pageViewRadio.checked;
			args.crop = cropRadio.checked;
			args.fit = fitRadio.checked;

			return (fn != null) ? fn(!print, args) : editorUi.print(!print, args);
		};

		if ((!editorUi.isOffline() || mxClient.IS_CHROMEAPP) && fn == null)
		{
			buttons.appendChild(editorUi.createHelpIcon(
				'https://www.drawio.com/doc/faq/print-diagram'));
		}
		
		var cancelBtn = mxUtils.button(mxResources.get('cancel'), function()
		{
			editorUi.hideDialog();
		});
		
		cancelBtn.className = 'geBtn';
		
		if (editorUi.editor.cancelFirst)
		{
			buttons.appendChild(cancelBtn);
		}
		
		if (PrintDialog.previewEnabled && fn == null)
		{
			var previewBtn = mxUtils.button(mxResources.get('preview'), function()
			{
				try
				{
					editorUi.hideDialog();
					preview(false);
				}
				catch (e)
				{
					editorUi.handleError(e);
				}
			});

			previewBtn.className = 'geBtn';
			buttons.appendChild(previewBtn);
		}

		btnTitle = (btnTitle != null) ? btnTitle : (fn != null) ? 'export' :
			(!PrintDialog.previewEnabled) ? 'ok' : 'print';
		var printBtn = mxUtils.button(mxResources.get(btnTitle), function()
		{
			try
			{
				editorUi.hideDialog();
				preview(true);
			}
			catch (e)
			{
				editorUi.handleError(e);
			}
		});

		printBtn.className = 'geBtn gePrimaryBtn';
		buttons.appendChild(printBtn);
		
		if (!editorUi.editor.cancelFirst)
		{
			buttons.appendChild(cancelBtn);
		}

		div.appendChild(buttons);

		// Handles dark mode for standalone print dialog in lightbox
		if (editorUi.editor.isChromelessView())
		{
			function updateDarkMode()
			{
				if (Editor.isDarkMode())
				{
					div.classList.add('geDarkMode');
					div.style.colorScheme = 'dark';
				}
				else
				{
					div.classList.remove('geDarkMode');
					div.style.colorScheme = 'light';
				}
			};

			editorUi.addListener('darkModeChanged', updateDarkMode);
			updateDarkMode();

			// Removes listeners after dialog was closed
			var removeListener = function(sender, evt)
			{
				var dialog = evt.getProperty('dialog');

				if (div.parentNode == dialog.container)
				{
					editorUi.editor.removeListener(removeListener);
					editorUi.removeListener(updateDarkMode);
				}
			};

			editorUi.editor.addListener('hideDialog', removeListener);
		}

		this.container = div;
	};
	
    // Execute fit page on page setup changes
    var changePageSetupExecute = ChangePageSetup.prototype.execute;
    
    ChangePageSetup.prototype.execute = function()
    {
        if (this.page == null)
        {
            this.page = this.ui.currentPage;
        }

        // Workaround for redo existing change with different current page
        if (this.page != this.ui.currentPage)
        {
            if (this.page.viewState != null)
            {
                if (!this.ignoreColor)
                {
                    this.page.viewState.background = this.color;
                }
                
                if (!this.ignoreImage)
                {
					var img = this.image;

					if (img != null && img.src != null && Graph.isPageLink(img.src))
					{
						img = {originalSrc: img.src};
					}

                    this.page.viewState.backgroundImage = img;
                }

                if (this.format != null)
                {
                    this.page.viewState.pageFormat = this.format;
                }
                
                if (this.mathEnabled != null)
                {
                    this.page.viewState.mathEnabled = this.mathEnabled;
                }
                
                if (this.adaptiveColors != null)
				{
					this.page.viewState.adaptiveColors = this.adaptiveColors;
				}
				
                if (this.shadowVisible != null)
            	{
            		this.page.viewState.shadowVisible = this.shadowVisible;
            	}
            }   
        }
        else
        {
            changePageSetupExecute.apply(this, arguments);
            
            if (this.mathEnabled != null && this.mathEnabled != this.ui.isMathEnabled())
            {
                this.ui.setMathEnabled(this.mathEnabled);
                this.mathEnabled = !this.mathEnabled;
            }
			
            if (this.adaptiveColors != null && this.adaptiveColors != this.ui.editor.graph.adaptiveColors)
			{
				var temp = this.ui.editor.graph.adaptiveColors
				this.ui.setAdaptiveColors(this.adaptiveColors);
				this.adaptiveColors = (temp != null) ? temp : 'default';
			}
			
            if (this.shadowVisible != null && this.shadowVisible != this.ui.editor.graph.shadowVisible)
            {
            	this.ui.editor.graph.setShadowVisible(this.shadowVisible);
                this.shadowVisible = !this.shadowVisible;
            }
        }
    };
})();
